##// END OF EJS Templates
Use explicit relative imports...
Thomas Kluyver -
Show More
@@ -1,4 +1,4 b''
1 1 try:
2 2 from decorator import *
3 3 except ImportError:
4 from _decorator import *
4 from ._decorator import *
@@ -1,9 +1,9 b''
1 1 try:
2 2 from numpy.testing.decorators import *
3 3 from numpy.testing.noseclasses import KnownFailure
4 4 except ImportError:
5 from _decorators import *
5 from ._decorators import *
6 6 try:
7 from _numpy_testing_noseclasses import KnownFailure
7 from ._numpy_testing_noseclasses import KnownFailure
8 8 except ImportError:
9 9 pass
@@ -1,281 +1,281 b''
1 1 """
2 2 Decorators for labeling and modifying behavior of test objects.
3 3
4 4 Decorators that merely return a modified version of the original
5 5 function object are straightforward. Decorators that return a new
6 6 function object need to use
7 7 ::
8 8
9 9 nose.tools.make_decorator(original_function)(decorator)
10 10
11 11 in returning the decorator, in order to preserve meta-data such as
12 12 function name, setup and teardown functions and so on - see
13 13 ``nose.tools`` for more information.
14 14
15 15 """
16 16 import warnings
17 17
18 18 # IPython changes: make this work if numpy not available
19 19 # Original code:
20 20 #from numpy.testing.utils import \
21 21 # WarningManager, WarningMessage
22 22 # Our version:
23 from _numpy_testing_utils import WarningManager
23 from ._numpy_testing_utils import WarningManager
24 24 try:
25 from _numpy_testing_noseclasses import KnownFailureTest
25 from ._numpy_testing_noseclasses import KnownFailureTest
26 26 except:
27 27 pass
28 28
29 29 # End IPython changes
30 30
31 31 def slow(t):
32 32 """
33 33 Label a test as 'slow'.
34 34
35 35 The exact definition of a slow test is obviously both subjective and
36 36 hardware-dependent, but in general any individual test that requires more
37 37 than a second or two should be labeled as slow (the whole suite consists of
38 38 thousands of tests, so even a second is significant).
39 39
40 40 Parameters
41 41 ----------
42 42 t : callable
43 43 The test to label as slow.
44 44
45 45 Returns
46 46 -------
47 47 t : callable
48 48 The decorated test `t`.
49 49
50 50 Examples
51 51 --------
52 52 The `numpy.testing` module includes ``import decorators as dec``.
53 53 A test can be decorated as slow like this::
54 54
55 55 from numpy.testing import *
56 56
57 57 @dec.slow
58 58 def test_big(self):
59 59 print 'Big, slow test'
60 60
61 61 """
62 62
63 63 t.slow = True
64 64 return t
65 65
66 66 def setastest(tf=True):
67 67 """
68 68 Signals to nose that this function is or is not a test.
69 69
70 70 Parameters
71 71 ----------
72 72 tf : bool
73 73 If True, specifies that the decorated callable is a test.
74 74 If False, specifies that the decorated callable is not a test.
75 75 Default is True.
76 76
77 77 Notes
78 78 -----
79 79 This decorator can't use the nose namespace, because it can be
80 80 called from a non-test module. See also ``istest`` and ``nottest`` in
81 81 ``nose.tools``.
82 82
83 83 Examples
84 84 --------
85 85 `setastest` can be used in the following way::
86 86
87 87 from numpy.testing.decorators import setastest
88 88
89 89 @setastest(False)
90 90 def func_with_test_in_name(arg1, arg2):
91 91 pass
92 92
93 93 """
94 94 def set_test(t):
95 95 t.__test__ = tf
96 96 return t
97 97 return set_test
98 98
99 99 def skipif(skip_condition, msg=None):
100 100 """
101 101 Make function raise SkipTest exception if a given condition is true.
102 102
103 103 If the condition is a callable, it is used at runtime to dynamically
104 104 make the decision. This is useful for tests that may require costly
105 105 imports, to delay the cost until the test suite is actually executed.
106 106
107 107 Parameters
108 108 ----------
109 109 skip_condition : bool or callable
110 110 Flag to determine whether to skip the decorated test.
111 111 msg : str, optional
112 112 Message to give on raising a SkipTest exception. Default is None.
113 113
114 114 Returns
115 115 -------
116 116 decorator : function
117 117 Decorator which, when applied to a function, causes SkipTest
118 118 to be raised when `skip_condition` is True, and the function
119 119 to be called normally otherwise.
120 120
121 121 Notes
122 122 -----
123 123 The decorator itself is decorated with the ``nose.tools.make_decorator``
124 124 function in order to transmit function name, and various other metadata.
125 125
126 126 """
127 127
128 128 def skip_decorator(f):
129 129 # Local import to avoid a hard nose dependency and only incur the
130 130 # import time overhead at actual test-time.
131 131 import nose
132 132
133 133 # Allow for both boolean or callable skip conditions.
134 134 if callable(skip_condition):
135 135 skip_val = lambda : skip_condition()
136 136 else:
137 137 skip_val = lambda : skip_condition
138 138
139 139 def get_msg(func,msg=None):
140 140 """Skip message with information about function being skipped."""
141 141 if msg is None:
142 142 out = 'Test skipped due to test condition'
143 143 else:
144 144 out = '\n'+msg
145 145
146 146 return "Skipping test: %s%s" % (func.__name__,out)
147 147
148 148 # We need to define *two* skippers because Python doesn't allow both
149 149 # return with value and yield inside the same function.
150 150 def skipper_func(*args, **kwargs):
151 151 """Skipper for normal test functions."""
152 152 if skip_val():
153 153 raise nose.SkipTest(get_msg(f,msg))
154 154 else:
155 155 return f(*args, **kwargs)
156 156
157 157 def skipper_gen(*args, **kwargs):
158 158 """Skipper for test generators."""
159 159 if skip_val():
160 160 raise nose.SkipTest(get_msg(f,msg))
161 161 else:
162 162 for x in f(*args, **kwargs):
163 163 yield x
164 164
165 165 # Choose the right skipper to use when building the actual decorator.
166 166 if nose.util.isgenerator(f):
167 167 skipper = skipper_gen
168 168 else:
169 169 skipper = skipper_func
170 170
171 171 return nose.tools.make_decorator(f)(skipper)
172 172
173 173 return skip_decorator
174 174
175 175 def knownfailureif(fail_condition, msg=None):
176 176 """
177 177 Make function raise KnownFailureTest exception if given condition is true.
178 178
179 179 If the condition is a callable, it is used at runtime to dynamically
180 180 make the decision. This is useful for tests that may require costly
181 181 imports, to delay the cost until the test suite is actually executed.
182 182
183 183 Parameters
184 184 ----------
185 185 fail_condition : bool or callable
186 186 Flag to determine whether to mark the decorated test as a known
187 187 failure (if True) or not (if False).
188 188 msg : str, optional
189 189 Message to give on raising a KnownFailureTest exception.
190 190 Default is None.
191 191
192 192 Returns
193 193 -------
194 194 decorator : function
195 195 Decorator, which, when applied to a function, causes SkipTest
196 196 to be raised when `skip_condition` is True, and the function
197 197 to be called normally otherwise.
198 198
199 199 Notes
200 200 -----
201 201 The decorator itself is decorated with the ``nose.tools.make_decorator``
202 202 function in order to transmit function name, and various other metadata.
203 203
204 204 """
205 205 if msg is None:
206 206 msg = 'Test skipped due to known failure'
207 207
208 208 # Allow for both boolean or callable known failure conditions.
209 209 if callable(fail_condition):
210 210 fail_val = lambda : fail_condition()
211 211 else:
212 212 fail_val = lambda : fail_condition
213 213
214 214 def knownfail_decorator(f):
215 215 # Local import to avoid a hard nose dependency and only incur the
216 216 # import time overhead at actual test-time.
217 217 import nose
218 218 def knownfailer(*args, **kwargs):
219 219 if fail_val():
220 220 raise KnownFailureTest(msg)
221 221 else:
222 222 return f(*args, **kwargs)
223 223 return nose.tools.make_decorator(f)(knownfailer)
224 224
225 225 return knownfail_decorator
226 226
227 227 def deprecated(conditional=True):
228 228 """
229 229 Filter deprecation warnings while running the test suite.
230 230
231 231 This decorator can be used to filter DeprecationWarning's, to avoid
232 232 printing them during the test suite run, while checking that the test
233 233 actually raises a DeprecationWarning.
234 234
235 235 Parameters
236 236 ----------
237 237 conditional : bool or callable, optional
238 238 Flag to determine whether to mark test as deprecated or not. If the
239 239 condition is a callable, it is used at runtime to dynamically make the
240 240 decision. Default is True.
241 241
242 242 Returns
243 243 -------
244 244 decorator : function
245 245 The `deprecated` decorator itself.
246 246
247 247 Notes
248 248 -----
249 249 .. versionadded:: 1.4.0
250 250
251 251 """
252 252 def deprecate_decorator(f):
253 253 # Local import to avoid a hard nose dependency and only incur the
254 254 # import time overhead at actual test-time.
255 255 import nose
256 256
257 257 def _deprecated_imp(*args, **kwargs):
258 258 # Poor man's replacement for the with statement
259 259 ctx = WarningManager(record=True)
260 260 l = ctx.__enter__()
261 261 warnings.simplefilter('always')
262 262 try:
263 263 f(*args, **kwargs)
264 264 if not len(l) > 0:
265 265 raise AssertionError("No warning raised when calling %s"
266 266 % f.__name__)
267 267 if not l[0].category is DeprecationWarning:
268 268 raise AssertionError("First warning for %s is not a " \
269 269 "DeprecationWarning( is %s)" % (f.__name__, l[0]))
270 270 finally:
271 271 ctx.__exit__()
272 272
273 273 if callable(conditional):
274 274 cond = conditional()
275 275 else:
276 276 cond = conditional
277 277 if cond:
278 278 return nose.tools.make_decorator(f)(_deprecated_imp)
279 279 else:
280 280 return f
281 281 return deprecate_decorator
@@ -1,4 +1,4 b''
1 1 try:
2 2 from jsonpointer import *
3 3 except ImportError :
4 from _jsonpointer import *
4 from ._jsonpointer import *
@@ -1,4 +1,4 b''
1 1 try:
2 2 from jsonschema import *
3 3 except ImportError :
4 from _jsonschema import *
4 from ._jsonschema import *
@@ -1,5 +1,5 b''
1 1 try:
2 2 import pexpect
3 3 from pexpect import *
4 4 except ImportError:
5 from _pexpect import *
5 from ._pexpect import *
@@ -1,4 +1,4 b''
1 1 try:
2 2 from simplegeneric import *
3 3 except ImportError:
4 from _simplegeneric import *
4 from ._simplegeneric import *
@@ -1,356 +1,356 b''
1 1 """Basic ssh tunnel utilities, and convenience functions for tunneling
2 2 zeromq connections.
3 3
4 4 Authors
5 5 -------
6 6 * Min RK
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2010-2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 from __future__ import print_function
23 23
24 24 import os,sys, atexit
25 25 import signal
26 26 import socket
27 27 from multiprocessing import Process
28 28 from getpass import getpass, getuser
29 29 import warnings
30 30
31 31 try:
32 32 with warnings.catch_warnings():
33 33 warnings.simplefilter('ignore', DeprecationWarning)
34 34 import paramiko
35 35 except ImportError:
36 36 paramiko = None
37 37 else:
38 from forward import forward_tunnel
38 from .forward import forward_tunnel
39 39
40 40 try:
41 41 from IPython.external import pexpect
42 42 except ImportError:
43 43 pexpect = None
44 44
45 45 #-----------------------------------------------------------------------------
46 46 # Code
47 47 #-----------------------------------------------------------------------------
48 48
49 49 # select_random_ports copied from IPython.parallel.util
50 50 _random_ports = set()
51 51
52 52 def select_random_ports(n):
53 53 """Selects and return n random ports that are available."""
54 54 ports = []
55 55 for i in xrange(n):
56 56 sock = socket.socket()
57 57 sock.bind(('', 0))
58 58 while sock.getsockname()[1] in _random_ports:
59 59 sock.close()
60 60 sock = socket.socket()
61 61 sock.bind(('', 0))
62 62 ports.append(sock)
63 63 for i, sock in enumerate(ports):
64 64 port = sock.getsockname()[1]
65 65 sock.close()
66 66 ports[i] = port
67 67 _random_ports.add(port)
68 68 return ports
69 69
70 70
71 71 #-----------------------------------------------------------------------------
72 72 # Check for passwordless login
73 73 #-----------------------------------------------------------------------------
74 74
75 75 def try_passwordless_ssh(server, keyfile, paramiko=None):
76 76 """Attempt to make an ssh connection without a password.
77 77 This is mainly used for requiring password input only once
78 78 when many tunnels may be connected to the same server.
79 79
80 80 If paramiko is None, the default for the platform is chosen.
81 81 """
82 82 if paramiko is None:
83 83 paramiko = sys.platform == 'win32'
84 84 if not paramiko:
85 85 f = _try_passwordless_openssh
86 86 else:
87 87 f = _try_passwordless_paramiko
88 88 return f(server, keyfile)
89 89
90 90 def _try_passwordless_openssh(server, keyfile):
91 91 """Try passwordless login with shell ssh command."""
92 92 if pexpect is None:
93 93 raise ImportError("pexpect unavailable, use paramiko")
94 94 cmd = 'ssh -f '+ server
95 95 if keyfile:
96 96 cmd += ' -i ' + keyfile
97 97 cmd += ' exit'
98 98 p = pexpect.spawn(cmd)
99 99 while True:
100 100 try:
101 101 p.expect('[Pp]assword:', timeout=.1)
102 102 except pexpect.TIMEOUT:
103 103 continue
104 104 except pexpect.EOF:
105 105 return True
106 106 else:
107 107 return False
108 108
109 109 def _try_passwordless_paramiko(server, keyfile):
110 110 """Try passwordless login with paramiko."""
111 111 if paramiko is None:
112 112 msg = "Paramiko unavaliable, "
113 113 if sys.platform == 'win32':
114 114 msg += "Paramiko is required for ssh tunneled connections on Windows."
115 115 else:
116 116 msg += "use OpenSSH."
117 117 raise ImportError(msg)
118 118 username, server, port = _split_server(server)
119 119 client = paramiko.SSHClient()
120 120 client.load_system_host_keys()
121 121 client.set_missing_host_key_policy(paramiko.WarningPolicy())
122 122 try:
123 123 client.connect(server, port, username=username, key_filename=keyfile,
124 124 look_for_keys=True)
125 125 except paramiko.AuthenticationException:
126 126 return False
127 127 else:
128 128 client.close()
129 129 return True
130 130
131 131
132 132 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
133 133 """Connect a socket to an address via an ssh tunnel.
134 134
135 135 This is a wrapper for socket.connect(addr), when addr is not accessible
136 136 from the local machine. It simply creates an ssh tunnel using the remaining args,
137 137 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
138 138 selected local port of the tunnel.
139 139
140 140 """
141 141 new_url, tunnel = open_tunnel(addr, server, keyfile=keyfile, password=password, paramiko=paramiko, timeout=timeout)
142 142 socket.connect(new_url)
143 143 return tunnel
144 144
145 145
146 146 def open_tunnel(addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
147 147 """Open a tunneled connection from a 0MQ url.
148 148
149 149 For use inside tunnel_connection.
150 150
151 151 Returns
152 152 -------
153 153
154 154 (url, tunnel): The 0MQ url that has been forwarded, and the tunnel object
155 155 """
156 156
157 157 lport = select_random_ports(1)[0]
158 158 transport, addr = addr.split('://')
159 159 ip,rport = addr.split(':')
160 160 rport = int(rport)
161 161 if paramiko is None:
162 162 paramiko = sys.platform == 'win32'
163 163 if paramiko:
164 164 tunnelf = paramiko_tunnel
165 165 else:
166 166 tunnelf = openssh_tunnel
167 167
168 168 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password, timeout=timeout)
169 169 return 'tcp://127.0.0.1:%i'%lport, tunnel
170 170
171 171 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
172 172 """Create an ssh tunnel using command-line ssh that connects port lport
173 173 on this machine to localhost:rport on server. The tunnel
174 174 will automatically close when not in use, remaining open
175 175 for a minimum of timeout seconds for an initial connection.
176 176
177 177 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
178 178 as seen from `server`.
179 179
180 180 keyfile and password may be specified, but ssh config is checked for defaults.
181 181
182 182 Parameters
183 183 ----------
184 184
185 185 lport : int
186 186 local port for connecting to the tunnel from this machine.
187 187 rport : int
188 188 port on the remote machine to connect to.
189 189 server : str
190 190 The ssh server to connect to. The full ssh server string will be parsed.
191 191 user@server:port
192 192 remoteip : str [Default: 127.0.0.1]
193 193 The remote ip, specifying the destination of the tunnel.
194 194 Default is localhost, which means that the tunnel would redirect
195 195 localhost:lport on this machine to localhost:rport on the *server*.
196 196
197 197 keyfile : str; path to public key file
198 198 This specifies a key to be used in ssh login, default None.
199 199 Regular default ssh keys will be used without specifying this argument.
200 200 password : str;
201 201 Your ssh password to the ssh server. Note that if this is left None,
202 202 you will be prompted for it if passwordless key based login is unavailable.
203 203 timeout : int [default: 60]
204 204 The time (in seconds) after which no activity will result in the tunnel
205 205 closing. This prevents orphaned tunnels from running forever.
206 206 """
207 207 if pexpect is None:
208 208 raise ImportError("pexpect unavailable, use paramiko_tunnel")
209 209 ssh="ssh "
210 210 if keyfile:
211 211 ssh += "-i " + keyfile
212 212
213 213 if ':' in server:
214 214 server, port = server.split(':')
215 215 ssh += " -p %s" % port
216 216
217 217 cmd = "%s -f -L 127.0.0.1:%i:%s:%i %s sleep %i" % (
218 218 ssh, lport, remoteip, rport, server, timeout)
219 219 tunnel = pexpect.spawn(cmd)
220 220 failed = False
221 221 while True:
222 222 try:
223 223 tunnel.expect('[Pp]assword:', timeout=.1)
224 224 except pexpect.TIMEOUT:
225 225 continue
226 226 except pexpect.EOF:
227 227 if tunnel.exitstatus:
228 228 print (tunnel.exitstatus)
229 229 print (tunnel.before)
230 230 print (tunnel.after)
231 231 raise RuntimeError("tunnel '%s' failed to start"%(cmd))
232 232 else:
233 233 return tunnel.pid
234 234 else:
235 235 if failed:
236 236 print("Password rejected, try again")
237 237 password=None
238 238 if password is None:
239 239 password = getpass("%s's password: "%(server))
240 240 tunnel.sendline(password)
241 241 failed = True
242 242
243 243 def _split_server(server):
244 244 if '@' in server:
245 245 username,server = server.split('@', 1)
246 246 else:
247 247 username = getuser()
248 248 if ':' in server:
249 249 server, port = server.split(':')
250 250 port = int(port)
251 251 else:
252 252 port = 22
253 253 return username, server, port
254 254
255 255 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
256 256 """launch a tunner with paramiko in a subprocess. This should only be used
257 257 when shell ssh is unavailable (e.g. Windows).
258 258
259 259 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
260 260 as seen from `server`.
261 261
262 262 If you are familiar with ssh tunnels, this creates the tunnel:
263 263
264 264 ssh server -L localhost:lport:remoteip:rport
265 265
266 266 keyfile and password may be specified, but ssh config is checked for defaults.
267 267
268 268
269 269 Parameters
270 270 ----------
271 271
272 272 lport : int
273 273 local port for connecting to the tunnel from this machine.
274 274 rport : int
275 275 port on the remote machine to connect to.
276 276 server : str
277 277 The ssh server to connect to. The full ssh server string will be parsed.
278 278 user@server:port
279 279 remoteip : str [Default: 127.0.0.1]
280 280 The remote ip, specifying the destination of the tunnel.
281 281 Default is localhost, which means that the tunnel would redirect
282 282 localhost:lport on this machine to localhost:rport on the *server*.
283 283
284 284 keyfile : str; path to public key file
285 285 This specifies a key to be used in ssh login, default None.
286 286 Regular default ssh keys will be used without specifying this argument.
287 287 password : str;
288 288 Your ssh password to the ssh server. Note that if this is left None,
289 289 you will be prompted for it if passwordless key based login is unavailable.
290 290 timeout : int [default: 60]
291 291 The time (in seconds) after which no activity will result in the tunnel
292 292 closing. This prevents orphaned tunnels from running forever.
293 293
294 294 """
295 295 if paramiko is None:
296 296 raise ImportError("Paramiko not available")
297 297
298 298 if password is None:
299 299 if not _try_passwordless_paramiko(server, keyfile):
300 300 password = getpass("%s's password: "%(server))
301 301
302 302 p = Process(target=_paramiko_tunnel,
303 303 args=(lport, rport, server, remoteip),
304 304 kwargs=dict(keyfile=keyfile, password=password))
305 305 p.daemon=False
306 306 p.start()
307 307 atexit.register(_shutdown_process, p)
308 308 return p
309 309
310 310 def _shutdown_process(p):
311 311 if p.is_alive():
312 312 p.terminate()
313 313
314 314 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
315 315 """Function for actually starting a paramiko tunnel, to be passed
316 316 to multiprocessing.Process(target=this), and not called directly.
317 317 """
318 318 username, server, port = _split_server(server)
319 319 client = paramiko.SSHClient()
320 320 client.load_system_host_keys()
321 321 client.set_missing_host_key_policy(paramiko.WarningPolicy())
322 322
323 323 try:
324 324 client.connect(server, port, username=username, key_filename=keyfile,
325 325 look_for_keys=True, password=password)
326 326 # except paramiko.AuthenticationException:
327 327 # if password is None:
328 328 # password = getpass("%s@%s's password: "%(username, server))
329 329 # client.connect(server, port, username=username, password=password)
330 330 # else:
331 331 # raise
332 332 except Exception as e:
333 333 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
334 334 sys.exit(1)
335 335
336 336 # Don't let SIGINT kill the tunnel subprocess
337 337 signal.signal(signal.SIGINT, signal.SIG_IGN)
338 338
339 339 try:
340 340 forward_tunnel(lport, remoteip, rport, client.get_transport())
341 341 except KeyboardInterrupt:
342 342 print ('SIGINT: Port forwarding stopped cleanly')
343 343 sys.exit(0)
344 344 except Exception as e:
345 345 print ("Port forwarding stopped uncleanly: %s"%e)
346 346 sys.exit(255)
347 347
348 348 if sys.platform == 'win32':
349 349 ssh_tunnel = paramiko_tunnel
350 350 else:
351 351 ssh_tunnel = openssh_tunnel
352 352
353 353
354 354 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
355 355
356 356
@@ -1,65 +1,65 b''
1 1 import __builtin__
2 2 import sys
3 3
4 4 from IPython.core.displayhook import DisplayHook
5 5 from IPython.kernel.inprocess.socket import SocketABC
6 6 from IPython.utils.jsonutil import encode_images
7 7 from IPython.utils.traitlets import Instance, Dict
8 from session import extract_header, Session
8 from .session import extract_header, Session
9 9
10 10 class ZMQDisplayHook(object):
11 11 """A simple displayhook that publishes the object's repr over a ZeroMQ
12 12 socket."""
13 13 topic=b'pyout'
14 14
15 15 def __init__(self, session, pub_socket):
16 16 self.session = session
17 17 self.pub_socket = pub_socket
18 18 self.parent_header = {}
19 19
20 20 def __call__(self, obj):
21 21 if obj is None:
22 22 return
23 23
24 24 __builtin__._ = obj
25 25 sys.stdout.flush()
26 26 sys.stderr.flush()
27 27 msg = self.session.send(self.pub_socket, u'pyout', {u'data':repr(obj)},
28 28 parent=self.parent_header, ident=self.topic)
29 29
30 30 def set_parent(self, parent):
31 31 self.parent_header = extract_header(parent)
32 32
33 33
34 34 class ZMQShellDisplayHook(DisplayHook):
35 35 """A displayhook subclass that publishes data using ZeroMQ. This is intended
36 36 to work with an InteractiveShell instance. It sends a dict of different
37 37 representations of the object."""
38 38 topic=None
39 39
40 40 session = Instance(Session)
41 41 pub_socket = Instance(SocketABC)
42 42 parent_header = Dict({})
43 43
44 44 def set_parent(self, parent):
45 45 """Set the parent for outbound messages."""
46 46 self.parent_header = extract_header(parent)
47 47
48 48 def start_displayhook(self):
49 49 self.msg = self.session.msg(u'pyout', {}, parent=self.parent_header)
50 50
51 51 def write_output_prompt(self):
52 52 """Write the output prompt."""
53 53 self.msg['content']['execution_count'] = self.prompt_count
54 54
55 55 def write_format_data(self, format_dict, md_dict=None):
56 56 self.msg['content']['data'] = encode_images(format_dict)
57 57 self.msg['content']['metadata'] = md_dict
58 58
59 59 def finish_displayhook(self):
60 60 """Finish up all displayhook activities."""
61 61 sys.stdout.flush()
62 62 sys.stderr.flush()
63 63 self.session.send(self.pub_socket, self.msg, ident=self.topic)
64 64 self.msg = None
65 65
@@ -1,57 +1,57 b''
1 1 """Simple function for embedding an IPython kernel
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 import sys
8 8
9 9 from IPython.utils.frame import extract_module_locals
10 10
11 from kernelapp import IPKernelApp
11 from .kernelapp import IPKernelApp
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Code
15 15 #-----------------------------------------------------------------------------
16 16
17 17 def embed_kernel(module=None, local_ns=None, **kwargs):
18 18 """Embed and start an IPython kernel in a given scope.
19 19
20 20 Parameters
21 21 ----------
22 22 module : ModuleType, optional
23 23 The module to load into IPython globals (default: caller)
24 24 local_ns : dict, optional
25 25 The namespace to load into IPython user namespace (default: caller)
26 26
27 27 kwargs : various, optional
28 28 Further keyword args are relayed to the IPKernelApp constructor,
29 29 allowing configuration of the Kernel. Will only have an effect
30 30 on the first embed_kernel call for a given process.
31 31
32 32 """
33 33 # get the app if it exists, or set it up if it doesn't
34 34 if IPKernelApp.initialized():
35 35 app = IPKernelApp.instance()
36 36 else:
37 37 app = IPKernelApp.instance(**kwargs)
38 38 app.initialize([])
39 39 # Undo unnecessary sys module mangling from init_sys_modules.
40 40 # This would not be necessary if we could prevent it
41 41 # in the first place by using a different InteractiveShell
42 42 # subclass, as in the regular embed case.
43 43 main = app.kernel.shell._orig_sys_modules_main_mod
44 44 if main is not None:
45 45 sys.modules[app.kernel.shell._orig_sys_modules_main_name] = main
46 46
47 47 # load the calling scope if not given
48 48 (caller_module, caller_locals) = extract_module_locals(1)
49 49 if module is None:
50 50 module = caller_module
51 51 if local_ns is None:
52 52 local_ns = caller_locals
53 53
54 54 app.kernel.user_module = module
55 55 app.kernel.user_ns = local_ns
56 56 app.shell.set_completer_frame()
57 57 app.start()
@@ -1,220 +1,220 b''
1 1 """wrappers for stdout/stderr forwarding over zmq
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2013 The IPython Development Team
6 6 #
7 7 # Distributed under the terms of the BSD License. The full license is in
8 8 # the file COPYING, distributed as part of this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 import os
12 12 import threading
13 13 import time
14 14 import uuid
15 15 from io import StringIO, UnsupportedOperation
16 16
17 17 import zmq
18 18
19 from session import extract_header
19 from .session import extract_header
20 20
21 21 from IPython.utils import py3compat
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Globals
25 25 #-----------------------------------------------------------------------------
26 26
27 27 MASTER = 0
28 28 CHILD = 1
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Stream classes
32 32 #-----------------------------------------------------------------------------
33 33
34 34 class OutStream(object):
35 35 """A file like object that publishes the stream to a 0MQ PUB socket."""
36 36
37 37 # The time interval between automatic flushes, in seconds.
38 38 _subprocess_flush_limit = 256
39 39 flush_interval = 0.05
40 40 topic=None
41 41
42 42 def __init__(self, session, pub_socket, name, pipe=True):
43 43 self.encoding = 'UTF-8'
44 44 self.session = session
45 45 self.pub_socket = pub_socket
46 46 self.name = name
47 47 self.topic = b'stream.' + py3compat.cast_bytes(name)
48 48 self.parent_header = {}
49 49 self._new_buffer()
50 50 self._buffer_lock = threading.Lock()
51 51 self._master_pid = os.getpid()
52 52 self._master_thread = threading.current_thread().ident
53 53 self._pipe_pid = os.getpid()
54 54 self._pipe_flag = pipe
55 55 if pipe:
56 56 self._setup_pipe_in()
57 57
58 58 def _setup_pipe_in(self):
59 59 """setup listening pipe for subprocesses"""
60 60 ctx = self.pub_socket.context
61 61
62 62 # use UUID to authenticate pipe messages
63 63 self._pipe_uuid = uuid.uuid4().bytes
64 64
65 65 self._pipe_in = ctx.socket(zmq.PULL)
66 66 self._pipe_in.linger = 0
67 67 self._pipe_port = self._pipe_in.bind_to_random_port("tcp://127.0.0.1")
68 68 self._pipe_poller = zmq.Poller()
69 69 self._pipe_poller.register(self._pipe_in, zmq.POLLIN)
70 70
71 71 def _setup_pipe_out(self):
72 72 # must be new context after fork
73 73 ctx = zmq.Context()
74 74 self._pipe_pid = os.getpid()
75 75 self._pipe_out = ctx.socket(zmq.PUSH)
76 76 self._pipe_out_lock = threading.Lock()
77 77 self._pipe_out.connect("tcp://127.0.0.1:%i" % self._pipe_port)
78 78
79 79 def _is_master_process(self):
80 80 return os.getpid() == self._master_pid
81 81
82 82 def _is_master_thread(self):
83 83 return threading.current_thread().ident == self._master_thread
84 84
85 85 def _have_pipe_out(self):
86 86 return os.getpid() == self._pipe_pid
87 87
88 88 def _check_mp_mode(self):
89 89 """check for forks, and switch to zmq pipeline if necessary"""
90 90 if not self._pipe_flag or self._is_master_process():
91 91 return MASTER
92 92 else:
93 93 if not self._have_pipe_out():
94 94 self._flush_buffer()
95 95 # setup a new out pipe
96 96 self._setup_pipe_out()
97 97 return CHILD
98 98
99 99 def set_parent(self, parent):
100 100 self.parent_header = extract_header(parent)
101 101
102 102 def close(self):
103 103 self.pub_socket = None
104 104
105 105 def _flush_from_subprocesses(self):
106 106 """flush possible pub data from subprocesses into my buffer"""
107 107 if not self._pipe_flag or not self._is_master_process():
108 108 return
109 109 for i in range(self._subprocess_flush_limit):
110 110 if self._pipe_poller.poll(0):
111 111 msg = self._pipe_in.recv_multipart()
112 112 if msg[0] != self._pipe_uuid:
113 113 continue
114 114 else:
115 115 self._buffer.write(msg[1].decode(self.encoding, 'replace'))
116 116 # this always means a flush,
117 117 # so reset our timer
118 118 self._start = 0
119 119 else:
120 120 break
121 121
122 122 def flush(self):
123 123 """trigger actual zmq send"""
124 124 if self.pub_socket is None:
125 125 raise ValueError(u'I/O operation on closed file')
126 126
127 127 mp_mode = self._check_mp_mode()
128 128
129 129 if mp_mode != CHILD:
130 130 # we are master
131 131 if not self._is_master_thread():
132 132 # sub-threads must not trigger flush,
133 133 # but at least they can force the timer.
134 134 self._start = 0
135 135 return
136 136
137 137 self._flush_from_subprocesses()
138 138 data = self._flush_buffer()
139 139
140 140 if data:
141 141 content = {u'name':self.name, u'data':data}
142 142 msg = self.session.send(self.pub_socket, u'stream', content=content,
143 143 parent=self.parent_header, ident=self.topic)
144 144
145 145 if hasattr(self.pub_socket, 'flush'):
146 146 # socket itself has flush (presumably ZMQStream)
147 147 self.pub_socket.flush()
148 148 else:
149 149 with self._pipe_out_lock:
150 150 string = self._flush_buffer()
151 151 tracker = self._pipe_out.send_multipart([
152 152 self._pipe_uuid,
153 153 string.encode(self.encoding, 'replace'),
154 154 ], copy=False, track=True)
155 155 try:
156 156 tracker.wait(1)
157 157 except:
158 158 pass
159 159
160 160 def isatty(self):
161 161 return False
162 162
163 163 def __next__(self):
164 164 raise IOError('Read not supported on a write only stream.')
165 165
166 166 if not py3compat.PY3:
167 167 next = __next__
168 168
169 169 def read(self, size=-1):
170 170 raise IOError('Read not supported on a write only stream.')
171 171
172 172 def readline(self, size=-1):
173 173 raise IOError('Read not supported on a write only stream.')
174 174
175 175 def fileno(self):
176 176 raise UnsupportedOperation("IOStream has no fileno.")
177 177
178 178 def write(self, string):
179 179 if self.pub_socket is None:
180 180 raise ValueError('I/O operation on closed file')
181 181 else:
182 182 # Make sure that we're handling unicode
183 183 if not isinstance(string, unicode):
184 184 string = string.decode(self.encoding, 'replace')
185 185
186 186 is_child = (self._check_mp_mode() == CHILD)
187 187 self._buffer.write(string)
188 188 if is_child:
189 189 # newlines imply flush in subprocesses
190 190 # mp.Pool cannot be trusted to flush promptly (or ever),
191 191 # and this helps.
192 192 if '\n' in string:
193 193 self.flush()
194 194 # do we want to check subprocess flushes on write?
195 195 # self._flush_from_subprocesses()
196 196 current_time = time.time()
197 197 if self._start < 0:
198 198 self._start = current_time
199 199 elif current_time - self._start > self.flush_interval:
200 200 self.flush()
201 201
202 202 def writelines(self, sequence):
203 203 if self.pub_socket is None:
204 204 raise ValueError('I/O operation on closed file')
205 205 else:
206 206 for string in sequence:
207 207 self.write(string)
208 208
209 209 def _flush_buffer(self):
210 210 """clear the current buffer and return the current buffer data"""
211 211 data = u''
212 212 if self._buffer is not None:
213 213 data = self._buffer.getvalue()
214 214 self._buffer.close()
215 215 self._new_buffer()
216 216 return data
217 217
218 218 def _new_buffer(self):
219 219 self._buffer = StringIO()
220 220 self._start = -1
@@ -1,789 +1,789 b''
1 1 #!/usr/bin/env python
2 2 """An interactive kernel that talks to frontends over 0MQ."""
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Imports
6 6 #-----------------------------------------------------------------------------
7 7 from __future__ import print_function
8 8
9 9 # Standard library imports
10 10 import __builtin__
11 11 import sys
12 12 import time
13 13 import traceback
14 14 import logging
15 15 import uuid
16 16
17 17 from datetime import datetime
18 18 from signal import (
19 19 signal, default_int_handler, SIGINT
20 20 )
21 21
22 22 # System library imports
23 23 import zmq
24 24 from zmq.eventloop import ioloop
25 25 from zmq.eventloop.zmqstream import ZMQStream
26 26
27 27 # Local imports
28 28 from IPython.config.configurable import Configurable
29 29 from IPython.core.error import StdinNotImplementedError
30 30 from IPython.core import release
31 31 from IPython.utils import py3compat
32 32 from IPython.utils.jsonutil import json_clean
33 33 from IPython.utils.traitlets import (
34 34 Any, Instance, Float, Dict, List, Set, Integer, Unicode,
35 35 Type
36 36 )
37 37
38 from serialize import serialize_object, unpack_apply_message
39 from session import Session
40 from zmqshell import ZMQInteractiveShell
38 from .serialize import serialize_object, unpack_apply_message
39 from .session import Session
40 from .zmqshell import ZMQInteractiveShell
41 41
42 42
43 43 #-----------------------------------------------------------------------------
44 44 # Main kernel class
45 45 #-----------------------------------------------------------------------------
46 46
47 47 protocol_version = list(release.kernel_protocol_version_info)
48 48 ipython_version = list(release.version_info)
49 49 language_version = list(sys.version_info[:3])
50 50
51 51
52 52 class Kernel(Configurable):
53 53
54 54 #---------------------------------------------------------------------------
55 55 # Kernel interface
56 56 #---------------------------------------------------------------------------
57 57
58 58 # attribute to override with a GUI
59 59 eventloop = Any(None)
60 60 def _eventloop_changed(self, name, old, new):
61 61 """schedule call to eventloop from IOLoop"""
62 62 loop = ioloop.IOLoop.instance()
63 63 loop.add_timeout(time.time()+0.1, self.enter_eventloop)
64 64
65 65 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
66 66 shell_class = Type(ZMQInteractiveShell)
67 67
68 68 session = Instance(Session)
69 69 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
70 70 shell_streams = List()
71 71 control_stream = Instance(ZMQStream)
72 72 iopub_socket = Instance(zmq.Socket)
73 73 stdin_socket = Instance(zmq.Socket)
74 74 log = Instance(logging.Logger)
75 75
76 76 user_module = Any()
77 77 def _user_module_changed(self, name, old, new):
78 78 if self.shell is not None:
79 79 self.shell.user_module = new
80 80
81 81 user_ns = Instance(dict, args=None, allow_none=True)
82 82 def _user_ns_changed(self, name, old, new):
83 83 if self.shell is not None:
84 84 self.shell.user_ns = new
85 85 self.shell.init_user_ns()
86 86
87 87 # identities:
88 88 int_id = Integer(-1)
89 89 ident = Unicode()
90 90
91 91 def _ident_default(self):
92 92 return unicode(uuid.uuid4())
93 93
94 94
95 95 # Private interface
96 96
97 97 # Time to sleep after flushing the stdout/err buffers in each execute
98 98 # cycle. While this introduces a hard limit on the minimal latency of the
99 99 # execute cycle, it helps prevent output synchronization problems for
100 100 # clients.
101 101 # Units are in seconds. The minimum zmq latency on local host is probably
102 102 # ~150 microseconds, set this to 500us for now. We may need to increase it
103 103 # a little if it's not enough after more interactive testing.
104 104 _execute_sleep = Float(0.0005, config=True)
105 105
106 106 # Frequency of the kernel's event loop.
107 107 # Units are in seconds, kernel subclasses for GUI toolkits may need to
108 108 # adapt to milliseconds.
109 109 _poll_interval = Float(0.05, config=True)
110 110
111 111 # If the shutdown was requested over the network, we leave here the
112 112 # necessary reply message so it can be sent by our registered atexit
113 113 # handler. This ensures that the reply is only sent to clients truly at
114 114 # the end of our shutdown process (which happens after the underlying
115 115 # IPython shell's own shutdown).
116 116 _shutdown_message = None
117 117
118 118 # This is a dict of port number that the kernel is listening on. It is set
119 119 # by record_ports and used by connect_request.
120 120 _recorded_ports = Dict()
121 121
122 122 # A reference to the Python builtin 'raw_input' function.
123 123 # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
124 124 _sys_raw_input = Any()
125 125 _sys_eval_input = Any()
126 126
127 127 # set of aborted msg_ids
128 128 aborted = Set()
129 129
130 130
131 131 def __init__(self, **kwargs):
132 132 super(Kernel, self).__init__(**kwargs)
133 133
134 134 # Initialize the InteractiveShell subclass
135 135 self.shell = self.shell_class.instance(parent=self,
136 136 profile_dir = self.profile_dir,
137 137 user_module = self.user_module,
138 138 user_ns = self.user_ns,
139 139 kernel = self,
140 140 )
141 141 self.shell.displayhook.session = self.session
142 142 self.shell.displayhook.pub_socket = self.iopub_socket
143 143 self.shell.displayhook.topic = self._topic('pyout')
144 144 self.shell.display_pub.session = self.session
145 145 self.shell.display_pub.pub_socket = self.iopub_socket
146 146 self.shell.data_pub.session = self.session
147 147 self.shell.data_pub.pub_socket = self.iopub_socket
148 148
149 149 # TMP - hack while developing
150 150 self.shell._reply_content = None
151 151
152 152 # Build dict of handlers for message types
153 153 msg_types = [ 'execute_request', 'complete_request',
154 154 'object_info_request', 'history_request',
155 155 'kernel_info_request',
156 156 'connect_request', 'shutdown_request',
157 157 'apply_request',
158 158 ]
159 159 self.shell_handlers = {}
160 160 for msg_type in msg_types:
161 161 self.shell_handlers[msg_type] = getattr(self, msg_type)
162 162
163 163 comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
164 164 comm_manager = self.shell.comm_manager
165 165 for msg_type in comm_msg_types:
166 166 self.shell_handlers[msg_type] = getattr(comm_manager, msg_type)
167 167
168 168 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
169 169 self.control_handlers = {}
170 170 for msg_type in control_msg_types:
171 171 self.control_handlers[msg_type] = getattr(self, msg_type)
172 172
173 173
174 174 def dispatch_control(self, msg):
175 175 """dispatch control requests"""
176 176 idents,msg = self.session.feed_identities(msg, copy=False)
177 177 try:
178 178 msg = self.session.unserialize(msg, content=True, copy=False)
179 179 except:
180 180 self.log.error("Invalid Control Message", exc_info=True)
181 181 return
182 182
183 183 self.log.debug("Control received: %s", msg)
184 184
185 185 header = msg['header']
186 186 msg_id = header['msg_id']
187 187 msg_type = header['msg_type']
188 188
189 189 handler = self.control_handlers.get(msg_type, None)
190 190 if handler is None:
191 191 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
192 192 else:
193 193 try:
194 194 handler(self.control_stream, idents, msg)
195 195 except Exception:
196 196 self.log.error("Exception in control handler:", exc_info=True)
197 197
198 198 def dispatch_shell(self, stream, msg):
199 199 """dispatch shell requests"""
200 200 # flush control requests first
201 201 if self.control_stream:
202 202 self.control_stream.flush()
203 203
204 204 idents,msg = self.session.feed_identities(msg, copy=False)
205 205 try:
206 206 msg = self.session.unserialize(msg, content=True, copy=False)
207 207 except:
208 208 self.log.error("Invalid Message", exc_info=True)
209 209 return
210 210
211 211 header = msg['header']
212 212 msg_id = header['msg_id']
213 213 msg_type = msg['header']['msg_type']
214 214
215 215 # Print some info about this message and leave a '--->' marker, so it's
216 216 # easier to trace visually the message chain when debugging. Each
217 217 # handler prints its message at the end.
218 218 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
219 219 self.log.debug(' Content: %s\n --->\n ', msg['content'])
220 220
221 221 if msg_id in self.aborted:
222 222 self.aborted.remove(msg_id)
223 223 # is it safe to assume a msg_id will not be resubmitted?
224 224 reply_type = msg_type.split('_')[0] + '_reply'
225 225 status = {'status' : 'aborted'}
226 226 md = {'engine' : self.ident}
227 227 md.update(status)
228 228 reply_msg = self.session.send(stream, reply_type, metadata=md,
229 229 content=status, parent=msg, ident=idents)
230 230 return
231 231
232 232 handler = self.shell_handlers.get(msg_type, None)
233 233 if handler is None:
234 234 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
235 235 else:
236 236 # ensure default_int_handler during handler call
237 237 sig = signal(SIGINT, default_int_handler)
238 238 try:
239 239 handler(stream, idents, msg)
240 240 except Exception:
241 241 self.log.error("Exception in message handler:", exc_info=True)
242 242 finally:
243 243 signal(SIGINT, sig)
244 244
245 245 def enter_eventloop(self):
246 246 """enter eventloop"""
247 247 self.log.info("entering eventloop")
248 248 # restore default_int_handler
249 249 signal(SIGINT, default_int_handler)
250 250 while self.eventloop is not None:
251 251 try:
252 252 self.eventloop(self)
253 253 except KeyboardInterrupt:
254 254 # Ctrl-C shouldn't crash the kernel
255 255 self.log.error("KeyboardInterrupt caught in kernel")
256 256 continue
257 257 else:
258 258 # eventloop exited cleanly, this means we should stop (right?)
259 259 self.eventloop = None
260 260 break
261 261 self.log.info("exiting eventloop")
262 262
263 263 def start(self):
264 264 """register dispatchers for streams"""
265 265 self.shell.exit_now = False
266 266 if self.control_stream:
267 267 self.control_stream.on_recv(self.dispatch_control, copy=False)
268 268
269 269 def make_dispatcher(stream):
270 270 def dispatcher(msg):
271 271 return self.dispatch_shell(stream, msg)
272 272 return dispatcher
273 273
274 274 for s in self.shell_streams:
275 275 s.on_recv(make_dispatcher(s), copy=False)
276 276
277 277 # publish idle status
278 278 self._publish_status('starting')
279 279
280 280 def do_one_iteration(self):
281 281 """step eventloop just once"""
282 282 if self.control_stream:
283 283 self.control_stream.flush()
284 284 for stream in self.shell_streams:
285 285 # handle at most one request per iteration
286 286 stream.flush(zmq.POLLIN, 1)
287 287 stream.flush(zmq.POLLOUT)
288 288
289 289
290 290 def record_ports(self, ports):
291 291 """Record the ports that this kernel is using.
292 292
293 293 The creator of the Kernel instance must call this methods if they
294 294 want the :meth:`connect_request` method to return the port numbers.
295 295 """
296 296 self._recorded_ports = ports
297 297
298 298 #---------------------------------------------------------------------------
299 299 # Kernel request handlers
300 300 #---------------------------------------------------------------------------
301 301
302 302 def _make_metadata(self, other=None):
303 303 """init metadata dict, for execute/apply_reply"""
304 304 new_md = {
305 305 'dependencies_met' : True,
306 306 'engine' : self.ident,
307 307 'started': datetime.now(),
308 308 }
309 309 if other:
310 310 new_md.update(other)
311 311 return new_md
312 312
313 313 def _publish_pyin(self, code, parent, execution_count):
314 314 """Publish the code request on the pyin stream."""
315 315
316 316 self.session.send(self.iopub_socket, u'pyin',
317 317 {u'code':code, u'execution_count': execution_count},
318 318 parent=parent, ident=self._topic('pyin')
319 319 )
320 320
321 321 def _publish_status(self, status, parent=None):
322 322 """send status (busy/idle) on IOPub"""
323 323 self.session.send(self.iopub_socket,
324 324 u'status',
325 325 {u'execution_state': status},
326 326 parent=parent,
327 327 ident=self._topic('status'),
328 328 )
329 329
330 330
331 331 def execute_request(self, stream, ident, parent):
332 332 """handle an execute_request"""
333 333
334 334 self._publish_status(u'busy', parent)
335 335
336 336 try:
337 337 content = parent[u'content']
338 338 code = content[u'code']
339 339 silent = content[u'silent']
340 340 store_history = content.get(u'store_history', not silent)
341 341 except:
342 342 self.log.error("Got bad msg: ")
343 343 self.log.error("%s", parent)
344 344 return
345 345
346 346 md = self._make_metadata(parent['metadata'])
347 347
348 348 shell = self.shell # we'll need this a lot here
349 349
350 350 # Replace raw_input. Note that is not sufficient to replace
351 351 # raw_input in the user namespace.
352 352 if content.get('allow_stdin', False):
353 353 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
354 354 input = lambda prompt='': eval(raw_input(prompt))
355 355 else:
356 356 raw_input = input = lambda prompt='' : self._no_raw_input()
357 357
358 358 if py3compat.PY3:
359 359 self._sys_raw_input = __builtin__.input
360 360 __builtin__.input = raw_input
361 361 else:
362 362 self._sys_raw_input = __builtin__.raw_input
363 363 self._sys_eval_input = __builtin__.input
364 364 __builtin__.raw_input = raw_input
365 365 __builtin__.input = input
366 366
367 367 # Set the parent message of the display hook and out streams.
368 368 shell.set_parent(parent)
369 369
370 370 # Re-broadcast our input for the benefit of listening clients, and
371 371 # start computing output
372 372 if not silent:
373 373 self._publish_pyin(code, parent, shell.execution_count)
374 374
375 375 reply_content = {}
376 376 try:
377 377 # FIXME: the shell calls the exception handler itself.
378 378 shell.run_cell(code, store_history=store_history, silent=silent)
379 379 except:
380 380 status = u'error'
381 381 # FIXME: this code right now isn't being used yet by default,
382 382 # because the run_cell() call above directly fires off exception
383 383 # reporting. This code, therefore, is only active in the scenario
384 384 # where runlines itself has an unhandled exception. We need to
385 385 # uniformize this, for all exception construction to come from a
386 386 # single location in the codbase.
387 387 etype, evalue, tb = sys.exc_info()
388 388 tb_list = traceback.format_exception(etype, evalue, tb)
389 389 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
390 390 else:
391 391 status = u'ok'
392 392 finally:
393 393 # Restore raw_input.
394 394 if py3compat.PY3:
395 395 __builtin__.input = self._sys_raw_input
396 396 else:
397 397 __builtin__.raw_input = self._sys_raw_input
398 398 __builtin__.input = self._sys_eval_input
399 399
400 400 reply_content[u'status'] = status
401 401
402 402 # Return the execution counter so clients can display prompts
403 403 reply_content['execution_count'] = shell.execution_count - 1
404 404
405 405 # FIXME - fish exception info out of shell, possibly left there by
406 406 # runlines. We'll need to clean up this logic later.
407 407 if shell._reply_content is not None:
408 408 reply_content.update(shell._reply_content)
409 409 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
410 410 reply_content['engine_info'] = e_info
411 411 # reset after use
412 412 shell._reply_content = None
413 413
414 414 if 'traceback' in reply_content:
415 415 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
416 416
417 417
418 418 # At this point, we can tell whether the main code execution succeeded
419 419 # or not. If it did, we proceed to evaluate user_variables/expressions
420 420 if reply_content['status'] == 'ok':
421 421 reply_content[u'user_variables'] = \
422 422 shell.user_variables(content.get(u'user_variables', []))
423 423 reply_content[u'user_expressions'] = \
424 424 shell.user_expressions(content.get(u'user_expressions', {}))
425 425 else:
426 426 # If there was an error, don't even try to compute variables or
427 427 # expressions
428 428 reply_content[u'user_variables'] = {}
429 429 reply_content[u'user_expressions'] = {}
430 430
431 431 # Payloads should be retrieved regardless of outcome, so we can both
432 432 # recover partial output (that could have been generated early in a
433 433 # block, before an error) and clear the payload system always.
434 434 reply_content[u'payload'] = shell.payload_manager.read_payload()
435 435 # Be agressive about clearing the payload because we don't want
436 436 # it to sit in memory until the next execute_request comes in.
437 437 shell.payload_manager.clear_payload()
438 438
439 439 # Flush output before sending the reply.
440 440 sys.stdout.flush()
441 441 sys.stderr.flush()
442 442 # FIXME: on rare occasions, the flush doesn't seem to make it to the
443 443 # clients... This seems to mitigate the problem, but we definitely need
444 444 # to better understand what's going on.
445 445 if self._execute_sleep:
446 446 time.sleep(self._execute_sleep)
447 447
448 448 # Send the reply.
449 449 reply_content = json_clean(reply_content)
450 450
451 451 md['status'] = reply_content['status']
452 452 if reply_content['status'] == 'error' and \
453 453 reply_content['ename'] == 'UnmetDependency':
454 454 md['dependencies_met'] = False
455 455
456 456 reply_msg = self.session.send(stream, u'execute_reply',
457 457 reply_content, parent, metadata=md,
458 458 ident=ident)
459 459
460 460 self.log.debug("%s", reply_msg)
461 461
462 462 if not silent and reply_msg['content']['status'] == u'error':
463 463 self._abort_queues()
464 464
465 465 self._publish_status(u'idle', parent)
466 466
467 467 def complete_request(self, stream, ident, parent):
468 468 txt, matches = self._complete(parent)
469 469 matches = {'matches' : matches,
470 470 'matched_text' : txt,
471 471 'status' : 'ok'}
472 472 matches = json_clean(matches)
473 473 completion_msg = self.session.send(stream, 'complete_reply',
474 474 matches, parent, ident)
475 475 self.log.debug("%s", completion_msg)
476 476
477 477 def object_info_request(self, stream, ident, parent):
478 478 content = parent['content']
479 479 object_info = self.shell.object_inspect(content['oname'],
480 480 detail_level = content.get('detail_level', 0)
481 481 )
482 482 # Before we send this object over, we scrub it for JSON usage
483 483 oinfo = json_clean(object_info)
484 484 msg = self.session.send(stream, 'object_info_reply',
485 485 oinfo, parent, ident)
486 486 self.log.debug("%s", msg)
487 487
488 488 def history_request(self, stream, ident, parent):
489 489 # We need to pull these out, as passing **kwargs doesn't work with
490 490 # unicode keys before Python 2.6.5.
491 491 hist_access_type = parent['content']['hist_access_type']
492 492 raw = parent['content']['raw']
493 493 output = parent['content']['output']
494 494 if hist_access_type == 'tail':
495 495 n = parent['content']['n']
496 496 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
497 497 include_latest=True)
498 498
499 499 elif hist_access_type == 'range':
500 500 session = parent['content']['session']
501 501 start = parent['content']['start']
502 502 stop = parent['content']['stop']
503 503 hist = self.shell.history_manager.get_range(session, start, stop,
504 504 raw=raw, output=output)
505 505
506 506 elif hist_access_type == 'search':
507 507 n = parent['content'].get('n')
508 508 unique = parent['content'].get('unique', False)
509 509 pattern = parent['content']['pattern']
510 510 hist = self.shell.history_manager.search(
511 511 pattern, raw=raw, output=output, n=n, unique=unique)
512 512
513 513 else:
514 514 hist = []
515 515 hist = list(hist)
516 516 content = {'history' : hist}
517 517 content = json_clean(content)
518 518 msg = self.session.send(stream, 'history_reply',
519 519 content, parent, ident)
520 520 self.log.debug("Sending history reply with %i entries", len(hist))
521 521
522 522 def connect_request(self, stream, ident, parent):
523 523 if self._recorded_ports is not None:
524 524 content = self._recorded_ports.copy()
525 525 else:
526 526 content = {}
527 527 msg = self.session.send(stream, 'connect_reply',
528 528 content, parent, ident)
529 529 self.log.debug("%s", msg)
530 530
531 531 def kernel_info_request(self, stream, ident, parent):
532 532 vinfo = {
533 533 'protocol_version': protocol_version,
534 534 'ipython_version': ipython_version,
535 535 'language_version': language_version,
536 536 'language': 'python',
537 537 }
538 538 msg = self.session.send(stream, 'kernel_info_reply',
539 539 vinfo, parent, ident)
540 540 self.log.debug("%s", msg)
541 541
542 542 def shutdown_request(self, stream, ident, parent):
543 543 self.shell.exit_now = True
544 544 content = dict(status='ok')
545 545 content.update(parent['content'])
546 546 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
547 547 # same content, but different msg_id for broadcasting on IOPub
548 548 self._shutdown_message = self.session.msg(u'shutdown_reply',
549 549 content, parent
550 550 )
551 551
552 552 self._at_shutdown()
553 553 # call sys.exit after a short delay
554 554 loop = ioloop.IOLoop.instance()
555 555 loop.add_timeout(time.time()+0.1, loop.stop)
556 556
557 557 #---------------------------------------------------------------------------
558 558 # Engine methods
559 559 #---------------------------------------------------------------------------
560 560
561 561 def apply_request(self, stream, ident, parent):
562 562 try:
563 563 content = parent[u'content']
564 564 bufs = parent[u'buffers']
565 565 msg_id = parent['header']['msg_id']
566 566 except:
567 567 self.log.error("Got bad msg: %s", parent, exc_info=True)
568 568 return
569 569
570 570 self._publish_status(u'busy', parent)
571 571
572 572 # Set the parent message of the display hook and out streams.
573 573 shell = self.shell
574 574 shell.set_parent(parent)
575 575
576 576 # pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
577 577 # self.iopub_socket.send(pyin_msg)
578 578 # self.session.send(self.iopub_socket, u'pyin', {u'code':code},parent=parent)
579 579 md = self._make_metadata(parent['metadata'])
580 580 try:
581 581 working = shell.user_ns
582 582
583 583 prefix = "_"+str(msg_id).replace("-","")+"_"
584 584
585 585 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
586 586
587 587 fname = getattr(f, '__name__', 'f')
588 588
589 589 fname = prefix+"f"
590 590 argname = prefix+"args"
591 591 kwargname = prefix+"kwargs"
592 592 resultname = prefix+"result"
593 593
594 594 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
595 595 # print ns
596 596 working.update(ns)
597 597 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
598 598 try:
599 599 exec code in shell.user_global_ns, shell.user_ns
600 600 result = working.get(resultname)
601 601 finally:
602 602 for key in ns.iterkeys():
603 603 working.pop(key)
604 604
605 605 result_buf = serialize_object(result,
606 606 buffer_threshold=self.session.buffer_threshold,
607 607 item_threshold=self.session.item_threshold,
608 608 )
609 609
610 610 except:
611 611 # invoke IPython traceback formatting
612 612 shell.showtraceback()
613 613 # FIXME - fish exception info out of shell, possibly left there by
614 614 # run_code. We'll need to clean up this logic later.
615 615 reply_content = {}
616 616 if shell._reply_content is not None:
617 617 reply_content.update(shell._reply_content)
618 618 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
619 619 reply_content['engine_info'] = e_info
620 620 # reset after use
621 621 shell._reply_content = None
622 622
623 623 self.session.send(self.iopub_socket, u'pyerr', reply_content, parent=parent,
624 624 ident=self._topic('pyerr'))
625 625 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
626 626 result_buf = []
627 627
628 628 if reply_content['ename'] == 'UnmetDependency':
629 629 md['dependencies_met'] = False
630 630 else:
631 631 reply_content = {'status' : 'ok'}
632 632
633 633 # put 'ok'/'error' status in header, for scheduler introspection:
634 634 md['status'] = reply_content['status']
635 635
636 636 # flush i/o
637 637 sys.stdout.flush()
638 638 sys.stderr.flush()
639 639
640 640 reply_msg = self.session.send(stream, u'apply_reply', reply_content,
641 641 parent=parent, ident=ident,buffers=result_buf, metadata=md)
642 642
643 643 self._publish_status(u'idle', parent)
644 644
645 645 #---------------------------------------------------------------------------
646 646 # Control messages
647 647 #---------------------------------------------------------------------------
648 648
649 649 def abort_request(self, stream, ident, parent):
650 650 """abort a specifig msg by id"""
651 651 msg_ids = parent['content'].get('msg_ids', None)
652 652 if isinstance(msg_ids, basestring):
653 653 msg_ids = [msg_ids]
654 654 if not msg_ids:
655 655 self.abort_queues()
656 656 for mid in msg_ids:
657 657 self.aborted.add(str(mid))
658 658
659 659 content = dict(status='ok')
660 660 reply_msg = self.session.send(stream, 'abort_reply', content=content,
661 661 parent=parent, ident=ident)
662 662 self.log.debug("%s", reply_msg)
663 663
664 664 def clear_request(self, stream, idents, parent):
665 665 """Clear our namespace."""
666 666 self.shell.reset(False)
667 667 msg = self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
668 668 content = dict(status='ok'))
669 669
670 670
671 671 #---------------------------------------------------------------------------
672 672 # Protected interface
673 673 #---------------------------------------------------------------------------
674 674
675 675 def _wrap_exception(self, method=None):
676 676 # import here, because _wrap_exception is only used in parallel,
677 677 # and parallel has higher min pyzmq version
678 678 from IPython.parallel.error import wrap_exception
679 679 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method=method)
680 680 content = wrap_exception(e_info)
681 681 return content
682 682
683 683 def _topic(self, topic):
684 684 """prefixed topic for IOPub messages"""
685 685 if self.int_id >= 0:
686 686 base = "engine.%i" % self.int_id
687 687 else:
688 688 base = "kernel.%s" % self.ident
689 689
690 690 return py3compat.cast_bytes("%s.%s" % (base, topic))
691 691
692 692 def _abort_queues(self):
693 693 for stream in self.shell_streams:
694 694 if stream:
695 695 self._abort_queue(stream)
696 696
697 697 def _abort_queue(self, stream):
698 698 poller = zmq.Poller()
699 699 poller.register(stream.socket, zmq.POLLIN)
700 700 while True:
701 701 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
702 702 if msg is None:
703 703 return
704 704
705 705 self.log.info("Aborting:")
706 706 self.log.info("%s", msg)
707 707 msg_type = msg['header']['msg_type']
708 708 reply_type = msg_type.split('_')[0] + '_reply'
709 709
710 710 status = {'status' : 'aborted'}
711 711 md = {'engine' : self.ident}
712 712 md.update(status)
713 713 reply_msg = self.session.send(stream, reply_type, metadata=md,
714 714 content=status, parent=msg, ident=idents)
715 715 self.log.debug("%s", reply_msg)
716 716 # We need to wait a bit for requests to come in. This can probably
717 717 # be set shorter for true asynchronous clients.
718 718 poller.poll(50)
719 719
720 720
721 721 def _no_raw_input(self):
722 722 """Raise StdinNotImplentedError if active frontend doesn't support
723 723 stdin."""
724 724 raise StdinNotImplementedError("raw_input was called, but this "
725 725 "frontend does not support stdin.")
726 726
727 727 def _raw_input(self, prompt, ident, parent):
728 728 # Flush output before making the request.
729 729 sys.stderr.flush()
730 730 sys.stdout.flush()
731 731 # flush the stdin socket, to purge stale replies
732 732 while True:
733 733 try:
734 734 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
735 735 except zmq.ZMQError as e:
736 736 if e.errno == zmq.EAGAIN:
737 737 break
738 738 else:
739 739 raise
740 740
741 741 # Send the input request.
742 742 content = json_clean(dict(prompt=prompt))
743 743 self.session.send(self.stdin_socket, u'input_request', content, parent,
744 744 ident=ident)
745 745
746 746 # Await a response.
747 747 while True:
748 748 try:
749 749 ident, reply = self.session.recv(self.stdin_socket, 0)
750 750 except Exception:
751 751 self.log.warn("Invalid Message:", exc_info=True)
752 752 except KeyboardInterrupt:
753 753 # re-raise KeyboardInterrupt, to truncate traceback
754 754 raise KeyboardInterrupt
755 755 else:
756 756 break
757 757 try:
758 758 value = py3compat.unicode_to_str(reply['content']['value'])
759 759 except:
760 760 self.log.error("Got bad raw_input reply: ")
761 761 self.log.error("%s", parent)
762 762 value = ''
763 763 if value == '\x04':
764 764 # EOF
765 765 raise EOFError
766 766 return value
767 767
768 768 def _complete(self, msg):
769 769 c = msg['content']
770 770 try:
771 771 cpos = int(c['cursor_pos'])
772 772 except:
773 773 # If we don't get something that we can convert to an integer, at
774 774 # least attempt the completion guessing the cursor is at the end of
775 775 # the text, if there's any, and otherwise of the line
776 776 cpos = len(c['text'])
777 777 if cpos==0:
778 778 cpos = len(c['line'])
779 779 return self.shell.complete(c['text'], c['line'], cpos)
780 780
781 781 def _at_shutdown(self):
782 782 """Actions taken at shutdown by the kernel, called by python's atexit.
783 783 """
784 784 # io.rprint("Kernel at_shutdown") # dbg
785 785 if self._shutdown_message is not None:
786 786 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
787 787 self.log.debug("%s", self._shutdown_message)
788 788 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
789 789
@@ -1,473 +1,473 b''
1 1 """An Application for launching a kernel
2 2
3 3 Authors
4 4 -------
5 5 * MinRK
6 6 """
7 7 #-----------------------------------------------------------------------------
8 8 # Copyright (C) 2011 The IPython Development Team
9 9 #
10 10 # Distributed under the terms of the BSD License. The full license is in
11 11 # the file COPYING.txt, distributed as part of this software.
12 12 #-----------------------------------------------------------------------------
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17
18 18 from __future__ import print_function
19 19
20 20 # Standard library imports
21 21 import atexit
22 22 import json
23 23 import os
24 24 import sys
25 25 import signal
26 26
27 27 # System library imports
28 28 import zmq
29 29 from zmq.eventloop import ioloop
30 30 from zmq.eventloop.zmqstream import ZMQStream
31 31
32 32 # IPython imports
33 33 from IPython.core.ultratb import FormattedTB
34 34 from IPython.core.application import (
35 35 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
36 36 )
37 37 from IPython.core.profiledir import ProfileDir
38 38 from IPython.core.shellapp import (
39 39 InteractiveShellApp, shell_flags, shell_aliases
40 40 )
41 41 from IPython.utils import io
42 42 from IPython.utils.localinterfaces import localhost
43 43 from IPython.utils.path import filefind
44 44 from IPython.utils.py3compat import str_to_bytes
45 45 from IPython.utils.traitlets import (
46 46 Any, Instance, Dict, Unicode, Integer, Bool, CaselessStrEnum,
47 47 DottedObjectName,
48 48 )
49 49 from IPython.utils.importstring import import_item
50 50 from IPython.kernel import write_connection_file
51 51
52 52 # local imports
53 from heartbeat import Heartbeat
54 from ipkernel import Kernel
55 from parentpoller import ParentPollerUnix, ParentPollerWindows
56 from session import (
53 from .heartbeat import Heartbeat
54 from .ipkernel import Kernel
55 from .parentpoller import ParentPollerUnix, ParentPollerWindows
56 from .session import (
57 57 Session, session_flags, session_aliases, default_secure,
58 58 )
59 from zmqshell import ZMQInteractiveShell
59 from .zmqshell import ZMQInteractiveShell
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # Flags and Aliases
63 63 #-----------------------------------------------------------------------------
64 64
65 65 kernel_aliases = dict(base_aliases)
66 66 kernel_aliases.update({
67 67 'ip' : 'IPKernelApp.ip',
68 68 'hb' : 'IPKernelApp.hb_port',
69 69 'shell' : 'IPKernelApp.shell_port',
70 70 'iopub' : 'IPKernelApp.iopub_port',
71 71 'stdin' : 'IPKernelApp.stdin_port',
72 72 'control' : 'IPKernelApp.control_port',
73 73 'f' : 'IPKernelApp.connection_file',
74 74 'parent': 'IPKernelApp.parent_handle',
75 75 'transport': 'IPKernelApp.transport',
76 76 })
77 77 if sys.platform.startswith('win'):
78 78 kernel_aliases['interrupt'] = 'IPKernelApp.interrupt'
79 79
80 80 kernel_flags = dict(base_flags)
81 81 kernel_flags.update({
82 82 'no-stdout' : (
83 83 {'IPKernelApp' : {'no_stdout' : True}},
84 84 "redirect stdout to the null device"),
85 85 'no-stderr' : (
86 86 {'IPKernelApp' : {'no_stderr' : True}},
87 87 "redirect stderr to the null device"),
88 88 'pylab' : (
89 89 {'IPKernelApp' : {'pylab' : 'auto'}},
90 90 """Pre-load matplotlib and numpy for interactive use with
91 91 the default matplotlib backend."""),
92 92 })
93 93
94 94 # inherit flags&aliases for any IPython shell apps
95 95 kernel_aliases.update(shell_aliases)
96 96 kernel_flags.update(shell_flags)
97 97
98 98 # inherit flags&aliases for Sessions
99 99 kernel_aliases.update(session_aliases)
100 100 kernel_flags.update(session_flags)
101 101
102 102 _ctrl_c_message = """\
103 103 NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
104 104
105 105 To exit, you will have to explicitly quit this process, by either sending
106 106 "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
107 107
108 108 To read more about this, see https://github.com/ipython/ipython/issues/2049
109 109
110 110 """
111 111
112 112 #-----------------------------------------------------------------------------
113 113 # Application class for starting an IPython Kernel
114 114 #-----------------------------------------------------------------------------
115 115
116 116 class IPKernelApp(BaseIPythonApplication, InteractiveShellApp):
117 117 name='ipkernel'
118 118 aliases = Dict(kernel_aliases)
119 119 flags = Dict(kernel_flags)
120 120 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
121 121 # the kernel class, as an importstring
122 122 kernel_class = DottedObjectName('IPython.kernel.zmq.ipkernel.Kernel', config=True,
123 123 help="""The Kernel subclass to be used.
124 124
125 125 This should allow easy re-use of the IPKernelApp entry point
126 126 to configure and launch kernels other than IPython's own.
127 127 """)
128 128 kernel = Any()
129 129 poller = Any() # don't restrict this even though current pollers are all Threads
130 130 heartbeat = Instance(Heartbeat)
131 131 session = Instance('IPython.kernel.zmq.session.Session')
132 132 ports = Dict()
133 133
134 134 # ipkernel doesn't get its own config file
135 135 def _config_file_name_default(self):
136 136 return 'ipython_config.py'
137 137
138 138 # inherit config file name from parent:
139 139 parent_appname = Unicode(config=True)
140 140 def _parent_appname_changed(self, name, old, new):
141 141 if self.config_file_specified:
142 142 # it was manually specified, ignore
143 143 return
144 144 self.config_file_name = new.replace('-','_') + u'_config.py'
145 145 # don't let this count as specifying the config file
146 146 self.config_file_specified.remove(self.config_file_name)
147 147
148 148 # connection info:
149 149 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
150 150 ip = Unicode(config=True,
151 151 help="Set the IP or interface on which the kernel will listen.")
152 152 def _ip_default(self):
153 153 if self.transport == 'ipc':
154 154 if self.connection_file:
155 155 return os.path.splitext(self.abs_connection_file)[0] + '-ipc'
156 156 else:
157 157 return 'kernel-ipc'
158 158 else:
159 159 return localhost()
160 160
161 161 hb_port = Integer(0, config=True, help="set the heartbeat port [default: random]")
162 162 shell_port = Integer(0, config=True, help="set the shell (ROUTER) port [default: random]")
163 163 iopub_port = Integer(0, config=True, help="set the iopub (PUB) port [default: random]")
164 164 stdin_port = Integer(0, config=True, help="set the stdin (ROUTER) port [default: random]")
165 165 control_port = Integer(0, config=True, help="set the control (ROUTER) port [default: random]")
166 166 connection_file = Unicode('', config=True,
167 167 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
168 168
169 169 This file will contain the IP, ports, and authentication key needed to connect
170 170 clients to this kernel. By default, this file will be created in the security dir
171 171 of the current profile, but can be specified by absolute path.
172 172 """)
173 173 @property
174 174 def abs_connection_file(self):
175 175 if os.path.basename(self.connection_file) == self.connection_file:
176 176 return os.path.join(self.profile_dir.security_dir, self.connection_file)
177 177 else:
178 178 return self.connection_file
179 179
180 180
181 181 # streams, etc.
182 182 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
183 183 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
184 184 outstream_class = DottedObjectName('IPython.kernel.zmq.iostream.OutStream',
185 185 config=True, help="The importstring for the OutStream factory")
186 186 displayhook_class = DottedObjectName('IPython.kernel.zmq.displayhook.ZMQDisplayHook',
187 187 config=True, help="The importstring for the DisplayHook factory")
188 188
189 189 # polling
190 190 parent_handle = Integer(0, config=True,
191 191 help="""kill this process if its parent dies. On Windows, the argument
192 192 specifies the HANDLE of the parent process, otherwise it is simply boolean.
193 193 """)
194 194 interrupt = Integer(0, config=True,
195 195 help="""ONLY USED ON WINDOWS
196 196 Interrupt this process when the parent is signaled.
197 197 """)
198 198
199 199 def init_crash_handler(self):
200 200 # Install minimal exception handling
201 201 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
202 202 ostream=sys.__stdout__)
203 203
204 204 def init_poller(self):
205 205 if sys.platform == 'win32':
206 206 if self.interrupt or self.parent_handle:
207 207 self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
208 208 elif self.parent_handle:
209 209 self.poller = ParentPollerUnix()
210 210
211 211 def _bind_socket(self, s, port):
212 212 iface = '%s://%s' % (self.transport, self.ip)
213 213 if self.transport == 'tcp':
214 214 if port <= 0:
215 215 port = s.bind_to_random_port(iface)
216 216 else:
217 217 s.bind("tcp://%s:%i" % (self.ip, port))
218 218 elif self.transport == 'ipc':
219 219 if port <= 0:
220 220 port = 1
221 221 path = "%s-%i" % (self.ip, port)
222 222 while os.path.exists(path):
223 223 port = port + 1
224 224 path = "%s-%i" % (self.ip, port)
225 225 else:
226 226 path = "%s-%i" % (self.ip, port)
227 227 s.bind("ipc://%s" % path)
228 228 return port
229 229
230 230 def load_connection_file(self):
231 231 """load ip/port/hmac config from JSON connection file"""
232 232 try:
233 233 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
234 234 except IOError:
235 235 self.log.debug("Connection file not found: %s", self.connection_file)
236 236 # This means I own it, so I will clean it up:
237 237 atexit.register(self.cleanup_connection_file)
238 238 return
239 239 self.log.debug(u"Loading connection file %s", fname)
240 240 with open(fname) as f:
241 241 s = f.read()
242 242 cfg = json.loads(s)
243 243 self.transport = cfg.get('transport', self.transport)
244 244 if self.ip == self._ip_default() and 'ip' in cfg:
245 245 # not overridden by config or cl_args
246 246 self.ip = cfg['ip']
247 247 for channel in ('hb', 'shell', 'iopub', 'stdin', 'control'):
248 248 name = channel + '_port'
249 249 if getattr(self, name) == 0 and name in cfg:
250 250 # not overridden by config or cl_args
251 251 setattr(self, name, cfg[name])
252 252 if 'key' in cfg:
253 253 self.config.Session.key = str_to_bytes(cfg['key'])
254 254
255 255 def write_connection_file(self):
256 256 """write connection info to JSON file"""
257 257 cf = self.abs_connection_file
258 258 self.log.debug("Writing connection file: %s", cf)
259 259 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
260 260 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
261 261 iopub_port=self.iopub_port, control_port=self.control_port)
262 262
263 263 def cleanup_connection_file(self):
264 264 cf = self.abs_connection_file
265 265 self.log.debug("Cleaning up connection file: %s", cf)
266 266 try:
267 267 os.remove(cf)
268 268 except (IOError, OSError):
269 269 pass
270 270
271 271 self.cleanup_ipc_files()
272 272
273 273 def cleanup_ipc_files(self):
274 274 """cleanup ipc files if we wrote them"""
275 275 if self.transport != 'ipc':
276 276 return
277 277 for port in (self.shell_port, self.iopub_port, self.stdin_port, self.hb_port, self.control_port):
278 278 ipcfile = "%s-%i" % (self.ip, port)
279 279 try:
280 280 os.remove(ipcfile)
281 281 except (IOError, OSError):
282 282 pass
283 283
284 284 def init_connection_file(self):
285 285 if not self.connection_file:
286 286 self.connection_file = "kernel-%s.json"%os.getpid()
287 287 try:
288 288 self.load_connection_file()
289 289 except Exception:
290 290 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
291 291 self.exit(1)
292 292
293 293 def init_sockets(self):
294 294 # Create a context, a session, and the kernel sockets.
295 295 self.log.info("Starting the kernel at pid: %i", os.getpid())
296 296 context = zmq.Context.instance()
297 297 # Uncomment this to try closing the context.
298 298 # atexit.register(context.term)
299 299
300 300 self.shell_socket = context.socket(zmq.ROUTER)
301 301 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
302 302 self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port)
303 303
304 304 self.iopub_socket = context.socket(zmq.PUB)
305 305 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
306 306 self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port)
307 307
308 308 self.stdin_socket = context.socket(zmq.ROUTER)
309 309 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
310 310 self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port)
311 311
312 312 self.control_socket = context.socket(zmq.ROUTER)
313 313 self.control_port = self._bind_socket(self.control_socket, self.control_port)
314 314 self.log.debug("control ROUTER Channel on port: %i" % self.control_port)
315 315
316 316 def init_heartbeat(self):
317 317 """start the heart beating"""
318 318 # heartbeat doesn't share context, because it mustn't be blocked
319 319 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
320 320 hb_ctx = zmq.Context()
321 321 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
322 322 self.hb_port = self.heartbeat.port
323 323 self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port)
324 324 self.heartbeat.start()
325 325
326 326 def log_connection_info(self):
327 327 """display connection info, and store ports"""
328 328 basename = os.path.basename(self.connection_file)
329 329 if basename == self.connection_file or \
330 330 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
331 331 # use shortname
332 332 tail = basename
333 333 if self.profile != 'default':
334 334 tail += " --profile %s" % self.profile
335 335 else:
336 336 tail = self.connection_file
337 337 lines = [
338 338 "To connect another client to this kernel, use:",
339 339 " --existing %s" % tail,
340 340 ]
341 341 # log connection info
342 342 # info-level, so often not shown.
343 343 # frontends should use the %connect_info magic
344 344 # to see the connection info
345 345 for line in lines:
346 346 self.log.info(line)
347 347 # also raw print to the terminal if no parent_handle (`ipython kernel`)
348 348 if not self.parent_handle:
349 349 io.rprint(_ctrl_c_message)
350 350 for line in lines:
351 351 io.rprint(line)
352 352
353 353 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
354 354 stdin=self.stdin_port, hb=self.hb_port,
355 355 control=self.control_port)
356 356
357 357 def init_session(self):
358 358 """create our session object"""
359 359 default_secure(self.config)
360 360 self.session = Session(parent=self, username=u'kernel')
361 361
362 362 def init_blackhole(self):
363 363 """redirects stdout/stderr to devnull if necessary"""
364 364 if self.no_stdout or self.no_stderr:
365 365 blackhole = open(os.devnull, 'w')
366 366 if self.no_stdout:
367 367 sys.stdout = sys.__stdout__ = blackhole
368 368 if self.no_stderr:
369 369 sys.stderr = sys.__stderr__ = blackhole
370 370
371 371 def init_io(self):
372 372 """Redirect input streams and set a display hook."""
373 373 if self.outstream_class:
374 374 outstream_factory = import_item(str(self.outstream_class))
375 375 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
376 376 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
377 377 if self.displayhook_class:
378 378 displayhook_factory = import_item(str(self.displayhook_class))
379 379 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
380 380
381 381 def init_signal(self):
382 382 signal.signal(signal.SIGINT, signal.SIG_IGN)
383 383
384 384 def init_kernel(self):
385 385 """Create the Kernel object itself"""
386 386 shell_stream = ZMQStream(self.shell_socket)
387 387 control_stream = ZMQStream(self.control_socket)
388 388
389 389 kernel_factory = import_item(str(self.kernel_class))
390 390
391 391 kernel = kernel_factory(parent=self, session=self.session,
392 392 shell_streams=[shell_stream, control_stream],
393 393 iopub_socket=self.iopub_socket,
394 394 stdin_socket=self.stdin_socket,
395 395 log=self.log,
396 396 profile_dir=self.profile_dir,
397 397 user_ns=self.user_ns,
398 398 )
399 399 kernel.record_ports(self.ports)
400 400 self.kernel = kernel
401 401
402 402 def init_gui_pylab(self):
403 403 """Enable GUI event loop integration, taking pylab into account."""
404 404
405 405 # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
406 406 # to ensure that any exception is printed straight to stderr.
407 407 # Normally _showtraceback associates the reply with an execution,
408 408 # which means frontends will never draw it, as this exception
409 409 # is not associated with any execute request.
410 410
411 411 shell = self.shell
412 412 _showtraceback = shell._showtraceback
413 413 try:
414 414 # replace pyerr-sending traceback with stderr
415 415 def print_tb(etype, evalue, stb):
416 416 print ("GUI event loop or pylab initialization failed",
417 417 file=io.stderr)
418 418 print (shell.InteractiveTB.stb2text(stb), file=io.stderr)
419 419 shell._showtraceback = print_tb
420 420 InteractiveShellApp.init_gui_pylab(self)
421 421 finally:
422 422 shell._showtraceback = _showtraceback
423 423
424 424 def init_shell(self):
425 425 self.shell = self.kernel.shell
426 426 self.shell.configurables.append(self)
427 427
428 428 @catch_config_error
429 429 def initialize(self, argv=None):
430 430 super(IPKernelApp, self).initialize(argv)
431 431 self.init_blackhole()
432 432 self.init_connection_file()
433 433 self.init_session()
434 434 self.init_poller()
435 435 self.init_sockets()
436 436 self.init_heartbeat()
437 437 # writing/displaying connection info must be *after* init_sockets/heartbeat
438 438 self.log_connection_info()
439 439 self.write_connection_file()
440 440 self.init_io()
441 441 self.init_signal()
442 442 self.init_kernel()
443 443 # shell init steps
444 444 self.init_path()
445 445 self.init_shell()
446 446 self.init_gui_pylab()
447 447 self.init_extensions()
448 448 self.init_code()
449 449 # flush stdout/stderr, so that anything written to these streams during
450 450 # initialization do not get associated with the first execution request
451 451 sys.stdout.flush()
452 452 sys.stderr.flush()
453 453
454 454 def start(self):
455 455 if self.poller is not None:
456 456 self.poller.start()
457 457 self.kernel.start()
458 458 try:
459 459 ioloop.IOLoop.instance().start()
460 460 except KeyboardInterrupt:
461 461 pass
462 462
463 463 launch_new_instance = IPKernelApp.launch_instance
464 464
465 465 def main():
466 466 """Run an IPKernel as an application"""
467 467 app = IPKernelApp.instance()
468 468 app.initialize()
469 469 app.start()
470 470
471 471
472 472 if __name__ == '__main__':
473 473 main()
@@ -1,623 +1,623 b''
1 1 """A ZMQ-based subclass of InteractiveShell.
2 2
3 3 This code is meant to ease the refactoring of the base InteractiveShell into
4 4 something with a cleaner architecture for 2-process use, without actually
5 5 breaking InteractiveShell itself. So we're doing something a bit ugly, where
6 6 we subclass and override what we want to fix. Once this is working well, we
7 7 can go back to the base class and refactor the code for a cleaner inheritance
8 8 implementation that doesn't rely on so much monkeypatching.
9 9
10 10 But this lets us maintain a fully working IPython as we develop the new
11 11 machinery. This should thus be thought of as scaffolding.
12 12 """
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from __future__ import print_function
17 17
18 18 # Stdlib
19 19 import os
20 20 import sys
21 21 import time
22 22
23 23 # System library imports
24 24 from zmq.eventloop import ioloop
25 25
26 26 # Our own
27 27 from IPython.core.interactiveshell import (
28 28 InteractiveShell, InteractiveShellABC
29 29 )
30 30 from IPython.core import page
31 31 from IPython.core.autocall import ZMQExitAutocall
32 32 from IPython.core.displaypub import DisplayPublisher
33 33 from IPython.core.error import UsageError
34 34 from IPython.core.magics import MacroToEdit, CodeMagics
35 35 from IPython.core.magic import magics_class, line_magic, Magics
36 36 from IPython.core.payloadpage import install_payload_page
37 37 from IPython.display import display, Javascript
38 38 from IPython.kernel.inprocess.socket import SocketABC
39 39 from IPython.kernel import (
40 40 get_connection_file, get_connection_info, connect_qtconsole
41 41 )
42 42 from IPython.testing.skipdoctest import skip_doctest
43 43 from IPython.utils import openpy
44 44 from IPython.utils.jsonutil import json_clean, encode_images
45 45 from IPython.utils.process import arg_split
46 46 from IPython.utils import py3compat
47 47 from IPython.utils.traitlets import Instance, Type, Dict, CBool, CBytes, Any
48 48 from IPython.utils.warn import error
49 49 from IPython.kernel.zmq.displayhook import ZMQShellDisplayHook
50 50 from IPython.kernel.zmq.datapub import ZMQDataPublisher
51 51 from IPython.kernel.zmq.session import extract_header
52 52 from IPython.kernel.comm import CommManager
53 from session import Session
53 from .session import Session
54 54
55 55 #-----------------------------------------------------------------------------
56 56 # Functions and classes
57 57 #-----------------------------------------------------------------------------
58 58
59 59 class ZMQDisplayPublisher(DisplayPublisher):
60 60 """A display publisher that publishes data using a ZeroMQ PUB socket."""
61 61
62 62 session = Instance(Session)
63 63 pub_socket = Instance(SocketABC)
64 64 parent_header = Dict({})
65 65 topic = CBytes(b'display_data')
66 66
67 67 def set_parent(self, parent):
68 68 """Set the parent for outbound messages."""
69 69 self.parent_header = extract_header(parent)
70 70
71 71 def _flush_streams(self):
72 72 """flush IO Streams prior to display"""
73 73 sys.stdout.flush()
74 74 sys.stderr.flush()
75 75
76 76 def publish(self, source, data, metadata=None):
77 77 self._flush_streams()
78 78 if metadata is None:
79 79 metadata = {}
80 80 self._validate_data(source, data, metadata)
81 81 content = {}
82 82 content['source'] = source
83 83 content['data'] = encode_images(data)
84 84 content['metadata'] = metadata
85 85 self.session.send(
86 86 self.pub_socket, u'display_data', json_clean(content),
87 87 parent=self.parent_header, ident=self.topic,
88 88 )
89 89
90 90 def clear_output(self, wait=False):
91 91 content = dict(wait=wait)
92 92
93 93 print('\r', file=sys.stdout, end='')
94 94 print('\r', file=sys.stderr, end='')
95 95 self._flush_streams()
96 96
97 97 self.session.send(
98 98 self.pub_socket, u'clear_output', content,
99 99 parent=self.parent_header, ident=self.topic,
100 100 )
101 101
102 102 @magics_class
103 103 class KernelMagics(Magics):
104 104 #------------------------------------------------------------------------
105 105 # Magic overrides
106 106 #------------------------------------------------------------------------
107 107 # Once the base class stops inheriting from magic, this code needs to be
108 108 # moved into a separate machinery as well. For now, at least isolate here
109 109 # the magics which this class needs to implement differently from the base
110 110 # class, or that are unique to it.
111 111
112 112 @line_magic
113 113 def doctest_mode(self, parameter_s=''):
114 114 """Toggle doctest mode on and off.
115 115
116 116 This mode is intended to make IPython behave as much as possible like a
117 117 plain Python shell, from the perspective of how its prompts, exceptions
118 118 and output look. This makes it easy to copy and paste parts of a
119 119 session into doctests. It does so by:
120 120
121 121 - Changing the prompts to the classic ``>>>`` ones.
122 122 - Changing the exception reporting mode to 'Plain'.
123 123 - Disabling pretty-printing of output.
124 124
125 125 Note that IPython also supports the pasting of code snippets that have
126 126 leading '>>>' and '...' prompts in them. This means that you can paste
127 127 doctests from files or docstrings (even if they have leading
128 128 whitespace), and the code will execute correctly. You can then use
129 129 '%history -t' to see the translated history; this will give you the
130 130 input after removal of all the leading prompts and whitespace, which
131 131 can be pasted back into an editor.
132 132
133 133 With these features, you can switch into this mode easily whenever you
134 134 need to do testing and changes to doctests, without having to leave
135 135 your existing IPython session.
136 136 """
137 137
138 138 from IPython.utils.ipstruct import Struct
139 139
140 140 # Shorthands
141 141 shell = self.shell
142 142 disp_formatter = self.shell.display_formatter
143 143 ptformatter = disp_formatter.formatters['text/plain']
144 144 # dstore is a data store kept in the instance metadata bag to track any
145 145 # changes we make, so we can undo them later.
146 146 dstore = shell.meta.setdefault('doctest_mode', Struct())
147 147 save_dstore = dstore.setdefault
148 148
149 149 # save a few values we'll need to recover later
150 150 mode = save_dstore('mode', False)
151 151 save_dstore('rc_pprint', ptformatter.pprint)
152 152 save_dstore('rc_active_types',disp_formatter.active_types)
153 153 save_dstore('xmode', shell.InteractiveTB.mode)
154 154
155 155 if mode == False:
156 156 # turn on
157 157 ptformatter.pprint = False
158 158 disp_formatter.active_types = ['text/plain']
159 159 shell.magic('xmode Plain')
160 160 else:
161 161 # turn off
162 162 ptformatter.pprint = dstore.rc_pprint
163 163 disp_formatter.active_types = dstore.rc_active_types
164 164 shell.magic("xmode " + dstore.xmode)
165 165
166 166 # Store new mode and inform on console
167 167 dstore.mode = bool(1-int(mode))
168 168 mode_label = ['OFF','ON'][dstore.mode]
169 169 print('Doctest mode is:', mode_label)
170 170
171 171 # Send the payload back so that clients can modify their prompt display
172 172 payload = dict(
173 173 source='doctest_mode',
174 174 mode=dstore.mode)
175 175 shell.payload_manager.write_payload(payload)
176 176
177 177
178 178 _find_edit_target = CodeMagics._find_edit_target
179 179
180 180 @skip_doctest
181 181 @line_magic
182 182 def edit(self, parameter_s='', last_call=['','']):
183 183 """Bring up an editor and execute the resulting code.
184 184
185 185 Usage:
186 186 %edit [options] [args]
187 187
188 188 %edit runs an external text editor. You will need to set the command for
189 189 this editor via the ``TerminalInteractiveShell.editor`` option in your
190 190 configuration file before it will work.
191 191
192 192 This command allows you to conveniently edit multi-line code right in
193 193 your IPython session.
194 194
195 195 If called without arguments, %edit opens up an empty editor with a
196 196 temporary file and will execute the contents of this file when you
197 197 close it (don't forget to save it!).
198 198
199 199
200 200 Options:
201 201
202 202 -n <number>: open the editor at a specified line number. By default,
203 203 the IPython editor hook uses the unix syntax 'editor +N filename', but
204 204 you can configure this by providing your own modified hook if your
205 205 favorite editor supports line-number specifications with a different
206 206 syntax.
207 207
208 208 -p: this will call the editor with the same data as the previous time
209 209 it was used, regardless of how long ago (in your current session) it
210 210 was.
211 211
212 212 -r: use 'raw' input. This option only applies to input taken from the
213 213 user's history. By default, the 'processed' history is used, so that
214 214 magics are loaded in their transformed version to valid Python. If
215 215 this option is given, the raw input as typed as the command line is
216 216 used instead. When you exit the editor, it will be executed by
217 217 IPython's own processor.
218 218
219 219 -x: do not execute the edited code immediately upon exit. This is
220 220 mainly useful if you are editing programs which need to be called with
221 221 command line arguments, which you can then do using %run.
222 222
223 223
224 224 Arguments:
225 225
226 226 If arguments are given, the following possibilites exist:
227 227
228 228 - The arguments are numbers or pairs of colon-separated numbers (like
229 229 1 4:8 9). These are interpreted as lines of previous input to be
230 230 loaded into the editor. The syntax is the same of the %macro command.
231 231
232 232 - If the argument doesn't start with a number, it is evaluated as a
233 233 variable and its contents loaded into the editor. You can thus edit
234 234 any string which contains python code (including the result of
235 235 previous edits).
236 236
237 237 - If the argument is the name of an object (other than a string),
238 238 IPython will try to locate the file where it was defined and open the
239 239 editor at the point where it is defined. You can use `%edit function`
240 240 to load an editor exactly at the point where 'function' is defined,
241 241 edit it and have the file be executed automatically.
242 242
243 243 If the object is a macro (see %macro for details), this opens up your
244 244 specified editor with a temporary file containing the macro's data.
245 245 Upon exit, the macro is reloaded with the contents of the file.
246 246
247 247 Note: opening at an exact line is only supported under Unix, and some
248 248 editors (like kedit and gedit up to Gnome 2.8) do not understand the
249 249 '+NUMBER' parameter necessary for this feature. Good editors like
250 250 (X)Emacs, vi, jed, pico and joe all do.
251 251
252 252 - If the argument is not found as a variable, IPython will look for a
253 253 file with that name (adding .py if necessary) and load it into the
254 254 editor. It will execute its contents with execfile() when you exit,
255 255 loading any code in the file into your interactive namespace.
256 256
257 257 After executing your code, %edit will return as output the code you
258 258 typed in the editor (except when it was an existing file). This way
259 259 you can reload the code in further invocations of %edit as a variable,
260 260 via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of
261 261 the output.
262 262
263 263 Note that %edit is also available through the alias %ed.
264 264
265 265 This is an example of creating a simple function inside the editor and
266 266 then modifying it. First, start up the editor:
267 267
268 268 In [1]: ed
269 269 Editing... done. Executing edited code...
270 270 Out[1]: 'def foo():n print "foo() was defined in an editing session"n'
271 271
272 272 We can then call the function foo():
273 273
274 274 In [2]: foo()
275 275 foo() was defined in an editing session
276 276
277 277 Now we edit foo. IPython automatically loads the editor with the
278 278 (temporary) file where foo() was previously defined:
279 279
280 280 In [3]: ed foo
281 281 Editing... done. Executing edited code...
282 282
283 283 And if we call foo() again we get the modified version:
284 284
285 285 In [4]: foo()
286 286 foo() has now been changed!
287 287
288 288 Here is an example of how to edit a code snippet successive
289 289 times. First we call the editor:
290 290
291 291 In [5]: ed
292 292 Editing... done. Executing edited code...
293 293 hello
294 294 Out[5]: "print 'hello'n"
295 295
296 296 Now we call it again with the previous output (stored in _):
297 297
298 298 In [6]: ed _
299 299 Editing... done. Executing edited code...
300 300 hello world
301 301 Out[6]: "print 'hello world'n"
302 302
303 303 Now we call it with the output #8 (stored in _8, also as Out[8]):
304 304
305 305 In [7]: ed _8
306 306 Editing... done. Executing edited code...
307 307 hello again
308 308 Out[7]: "print 'hello again'n"
309 309 """
310 310
311 311 opts,args = self.parse_options(parameter_s,'prn:')
312 312
313 313 try:
314 314 filename, lineno, _ = CodeMagics._find_edit_target(self.shell, args, opts, last_call)
315 315 except MacroToEdit as e:
316 316 # TODO: Implement macro editing over 2 processes.
317 317 print("Macro editing not yet implemented in 2-process model.")
318 318 return
319 319
320 320 # Make sure we send to the client an absolute path, in case the working
321 321 # directory of client and kernel don't match
322 322 filename = os.path.abspath(filename)
323 323
324 324 payload = {
325 325 'source' : 'edit_magic',
326 326 'filename' : filename,
327 327 'line_number' : lineno
328 328 }
329 329 self.shell.payload_manager.write_payload(payload)
330 330
331 331 # A few magics that are adapted to the specifics of using pexpect and a
332 332 # remote terminal
333 333
334 334 @line_magic
335 335 def clear(self, arg_s):
336 336 """Clear the terminal."""
337 337 if os.name == 'posix':
338 338 self.shell.system("clear")
339 339 else:
340 340 self.shell.system("cls")
341 341
342 342 if os.name == 'nt':
343 343 # This is the usual name in windows
344 344 cls = line_magic('cls')(clear)
345 345
346 346 # Terminal pagers won't work over pexpect, but we do have our own pager
347 347
348 348 @line_magic
349 349 def less(self, arg_s):
350 350 """Show a file through the pager.
351 351
352 352 Files ending in .py are syntax-highlighted."""
353 353 if not arg_s:
354 354 raise UsageError('Missing filename.')
355 355
356 356 cont = open(arg_s).read()
357 357 if arg_s.endswith('.py'):
358 358 cont = self.shell.pycolorize(openpy.read_py_file(arg_s, skip_encoding_cookie=False))
359 359 else:
360 360 cont = open(arg_s).read()
361 361 page.page(cont)
362 362
363 363 more = line_magic('more')(less)
364 364
365 365 # Man calls a pager, so we also need to redefine it
366 366 if os.name == 'posix':
367 367 @line_magic
368 368 def man(self, arg_s):
369 369 """Find the man page for the given command and display in pager."""
370 370 page.page(self.shell.getoutput('man %s | col -b' % arg_s,
371 371 split=False))
372 372
373 373 @line_magic
374 374 def connect_info(self, arg_s):
375 375 """Print information for connecting other clients to this kernel
376 376
377 377 It will print the contents of this session's connection file, as well as
378 378 shortcuts for local clients.
379 379
380 380 In the simplest case, when called from the most recently launched kernel,
381 381 secondary clients can be connected, simply with:
382 382
383 383 $> ipython <app> --existing
384 384
385 385 """
386 386
387 387 from IPython.core.application import BaseIPythonApplication as BaseIPApp
388 388
389 389 if BaseIPApp.initialized():
390 390 app = BaseIPApp.instance()
391 391 security_dir = app.profile_dir.security_dir
392 392 profile = app.profile
393 393 else:
394 394 profile = 'default'
395 395 security_dir = ''
396 396
397 397 try:
398 398 connection_file = get_connection_file()
399 399 info = get_connection_info(unpack=False)
400 400 except Exception as e:
401 401 error("Could not get connection info: %r" % e)
402 402 return
403 403
404 404 # add profile flag for non-default profile
405 405 profile_flag = "--profile %s" % profile if profile != 'default' else ""
406 406
407 407 # if it's in the security dir, truncate to basename
408 408 if security_dir == os.path.dirname(connection_file):
409 409 connection_file = os.path.basename(connection_file)
410 410
411 411
412 412 print (info + '\n')
413 413 print ("Paste the above JSON into a file, and connect with:\n"
414 414 " $> ipython <app> --existing <file>\n"
415 415 "or, if you are local, you can connect with just:\n"
416 416 " $> ipython <app> --existing {0} {1}\n"
417 417 "or even just:\n"
418 418 " $> ipython <app> --existing {1}\n"
419 419 "if this is the most recent IPython session you have started.".format(
420 420 connection_file, profile_flag
421 421 )
422 422 )
423 423
424 424 @line_magic
425 425 def qtconsole(self, arg_s):
426 426 """Open a qtconsole connected to this kernel.
427 427
428 428 Useful for connecting a qtconsole to running notebooks, for better
429 429 debugging.
430 430 """
431 431
432 432 # %qtconsole should imply bind_kernel for engines:
433 433 try:
434 434 from IPython.parallel import bind_kernel
435 435 except ImportError:
436 436 # technically possible, because parallel has higher pyzmq min-version
437 437 pass
438 438 else:
439 439 bind_kernel()
440 440
441 441 try:
442 442 p = connect_qtconsole(argv=arg_split(arg_s, os.name=='posix'))
443 443 except Exception as e:
444 444 error("Could not start qtconsole: %r" % e)
445 445 return
446 446
447 447 @line_magic
448 448 def autosave(self, arg_s):
449 449 """Set the autosave interval in the notebook (in seconds).
450 450
451 451 The default value is 120, or two minutes.
452 452 ``%autosave 0`` will disable autosave.
453 453
454 454 This magic only has an effect when called from the notebook interface.
455 455 It has no effect when called in a startup file.
456 456 """
457 457
458 458 try:
459 459 interval = int(arg_s)
460 460 except ValueError:
461 461 raise UsageError("%%autosave requires an integer, got %r" % arg_s)
462 462
463 463 # javascript wants milliseconds
464 464 milliseconds = 1000 * interval
465 465 display(Javascript("IPython.notebook.set_autosave_interval(%i)" % milliseconds),
466 466 include=['application/javascript']
467 467 )
468 468 if interval:
469 469 print("Autosaving every %i seconds" % interval)
470 470 else:
471 471 print("Autosave disabled")
472 472
473 473
474 474 class ZMQInteractiveShell(InteractiveShell):
475 475 """A subclass of InteractiveShell for ZMQ."""
476 476
477 477 displayhook_class = Type(ZMQShellDisplayHook)
478 478 display_pub_class = Type(ZMQDisplayPublisher)
479 479 data_pub_class = Type(ZMQDataPublisher)
480 480 kernel = Any()
481 481 parent_header = Any()
482 482
483 483 # Override the traitlet in the parent class, because there's no point using
484 484 # readline for the kernel. Can be removed when the readline code is moved
485 485 # to the terminal frontend.
486 486 colors_force = CBool(True)
487 487 readline_use = CBool(False)
488 488 # autoindent has no meaning in a zmqshell, and attempting to enable it
489 489 # will print a warning in the absence of readline.
490 490 autoindent = CBool(False)
491 491
492 492 exiter = Instance(ZMQExitAutocall)
493 493 def _exiter_default(self):
494 494 return ZMQExitAutocall(self)
495 495
496 496 def _exit_now_changed(self, name, old, new):
497 497 """stop eventloop when exit_now fires"""
498 498 if new:
499 499 loop = ioloop.IOLoop.instance()
500 500 loop.add_timeout(time.time()+0.1, loop.stop)
501 501
502 502 keepkernel_on_exit = None
503 503
504 504 # Over ZeroMQ, GUI control isn't done with PyOS_InputHook as there is no
505 505 # interactive input being read; we provide event loop support in ipkernel
506 506 @staticmethod
507 507 def enable_gui(gui):
508 508 from .eventloops import enable_gui as real_enable_gui
509 509 try:
510 510 real_enable_gui(gui)
511 511 except ValueError as e:
512 512 raise UsageError("%s" % e)
513 513
514 514 def init_environment(self):
515 515 """Configure the user's environment.
516 516
517 517 """
518 518 env = os.environ
519 519 # These two ensure 'ls' produces nice coloring on BSD-derived systems
520 520 env['TERM'] = 'xterm-color'
521 521 env['CLICOLOR'] = '1'
522 522 # Since normal pagers don't work at all (over pexpect we don't have
523 523 # single-key control of the subprocess), try to disable paging in
524 524 # subprocesses as much as possible.
525 525 env['PAGER'] = 'cat'
526 526 env['GIT_PAGER'] = 'cat'
527 527
528 528 # And install the payload version of page.
529 529 install_payload_page()
530 530
531 531 def auto_rewrite_input(self, cmd):
532 532 """Called to show the auto-rewritten input for autocall and friends.
533 533
534 534 FIXME: this payload is currently not correctly processed by the
535 535 frontend.
536 536 """
537 537 new = self.prompt_manager.render('rewrite') + cmd
538 538 payload = dict(
539 539 source='auto_rewrite_input',
540 540 transformed_input=new,
541 541 )
542 542 self.payload_manager.write_payload(payload)
543 543
544 544 def ask_exit(self):
545 545 """Engage the exit actions."""
546 546 self.exit_now = True
547 547 payload = dict(
548 548 source='ask_exit',
549 549 exit=True,
550 550 keepkernel=self.keepkernel_on_exit,
551 551 )
552 552 self.payload_manager.write_payload(payload)
553 553
554 554 def _showtraceback(self, etype, evalue, stb):
555 555
556 556 exc_content = {
557 557 u'traceback' : stb,
558 558 u'ename' : unicode(etype.__name__),
559 559 u'evalue' : py3compat.safe_unicode(evalue),
560 560 }
561 561
562 562 dh = self.displayhook
563 563 # Send exception info over pub socket for other clients than the caller
564 564 # to pick up
565 565 topic = None
566 566 if dh.topic:
567 567 topic = dh.topic.replace(b'pyout', b'pyerr')
568 568
569 569 exc_msg = dh.session.send(dh.pub_socket, u'pyerr', json_clean(exc_content), dh.parent_header, ident=topic)
570 570
571 571 # FIXME - Hack: store exception info in shell object. Right now, the
572 572 # caller is reading this info after the fact, we need to fix this logic
573 573 # to remove this hack. Even uglier, we need to store the error status
574 574 # here, because in the main loop, the logic that sets it is being
575 575 # skipped because runlines swallows the exceptions.
576 576 exc_content[u'status'] = u'error'
577 577 self._reply_content = exc_content
578 578 # /FIXME
579 579
580 580 return exc_content
581 581
582 582 def set_next_input(self, text):
583 583 """Send the specified text to the frontend to be presented at the next
584 584 input cell."""
585 585 payload = dict(
586 586 source='set_next_input',
587 587 text=text
588 588 )
589 589 self.payload_manager.write_payload(payload)
590 590
591 591 def set_parent(self, parent):
592 592 """Set the parent header for associating output with its triggering input"""
593 593 self.parent_header = parent
594 594 self.displayhook.set_parent(parent)
595 595 self.display_pub.set_parent(parent)
596 596 self.data_pub.set_parent(parent)
597 597 try:
598 598 sys.stdout.set_parent(parent)
599 599 except AttributeError:
600 600 pass
601 601 try:
602 602 sys.stderr.set_parent(parent)
603 603 except AttributeError:
604 604 pass
605 605
606 606 def get_parent(self):
607 607 return self.parent_header
608 608
609 609 #-------------------------------------------------------------------------
610 610 # Things related to magics
611 611 #-------------------------------------------------------------------------
612 612
613 613 def init_magics(self):
614 614 super(ZMQInteractiveShell, self).init_magics()
615 615 self.register_magics(KernelMagics)
616 616 self.magics_manager.register_alias('ed', 'edit')
617 617
618 618 def init_comms(self):
619 619 self.comm_manager = CommManager(shell=self, parent=self)
620 620 self.configurables.append(self.comm_manager)
621 621
622 622
623 623 InteractiveShellABC.register(ZMQInteractiveShell)
@@ -1,7 +1,7 b''
1 1 """Utilities for converting notebooks to and from different formats."""
2 2
3 3 from .exporters import *
4 import filters
5 import preprocessors
6 import postprocessors
7 import writers
4 from . import filters
5 from . import preprocessors
6 from . import postprocessors
7 from . import writers
@@ -1,517 +1,517 b''
1 1 """Tests for parallel client.py
2 2
3 3 Authors:
4 4
5 5 * Min RK
6 6 """
7 7
8 8 #-------------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-------------------------------------------------------------------------------
14 14
15 15 #-------------------------------------------------------------------------------
16 16 # Imports
17 17 #-------------------------------------------------------------------------------
18 18
19 19 from __future__ import division
20 20
21 21 import time
22 22 from datetime import datetime
23 23 from tempfile import mktemp
24 24
25 25 import zmq
26 26
27 27 from IPython import parallel
28 28 from IPython.parallel.client import client as clientmod
29 29 from IPython.parallel import error
30 30 from IPython.parallel import AsyncResult, AsyncHubResult
31 31 from IPython.parallel import LoadBalancedView, DirectView
32 32
33 from clienttest import ClusterTestCase, segfault, wait, add_engines
33 from .clienttest import ClusterTestCase, segfault, wait, add_engines
34 34
35 35 def setup():
36 36 add_engines(4, total=True)
37 37
38 38 class TestClient(ClusterTestCase):
39 39
40 40 def test_ids(self):
41 41 n = len(self.client.ids)
42 42 self.add_engines(2)
43 43 self.assertEqual(len(self.client.ids), n+2)
44 44
45 45 def test_view_indexing(self):
46 46 """test index access for views"""
47 47 self.minimum_engines(4)
48 48 targets = self.client._build_targets('all')[-1]
49 49 v = self.client[:]
50 50 self.assertEqual(v.targets, targets)
51 51 t = self.client.ids[2]
52 52 v = self.client[t]
53 53 self.assertTrue(isinstance(v, DirectView))
54 54 self.assertEqual(v.targets, t)
55 55 t = self.client.ids[2:4]
56 56 v = self.client[t]
57 57 self.assertTrue(isinstance(v, DirectView))
58 58 self.assertEqual(v.targets, t)
59 59 v = self.client[::2]
60 60 self.assertTrue(isinstance(v, DirectView))
61 61 self.assertEqual(v.targets, targets[::2])
62 62 v = self.client[1::3]
63 63 self.assertTrue(isinstance(v, DirectView))
64 64 self.assertEqual(v.targets, targets[1::3])
65 65 v = self.client[:-3]
66 66 self.assertTrue(isinstance(v, DirectView))
67 67 self.assertEqual(v.targets, targets[:-3])
68 68 v = self.client[-1]
69 69 self.assertTrue(isinstance(v, DirectView))
70 70 self.assertEqual(v.targets, targets[-1])
71 71 self.assertRaises(TypeError, lambda : self.client[None])
72 72
73 73 def test_lbview_targets(self):
74 74 """test load_balanced_view targets"""
75 75 v = self.client.load_balanced_view()
76 76 self.assertEqual(v.targets, None)
77 77 v = self.client.load_balanced_view(-1)
78 78 self.assertEqual(v.targets, [self.client.ids[-1]])
79 79 v = self.client.load_balanced_view('all')
80 80 self.assertEqual(v.targets, None)
81 81
82 82 def test_dview_targets(self):
83 83 """test direct_view targets"""
84 84 v = self.client.direct_view()
85 85 self.assertEqual(v.targets, 'all')
86 86 v = self.client.direct_view('all')
87 87 self.assertEqual(v.targets, 'all')
88 88 v = self.client.direct_view(-1)
89 89 self.assertEqual(v.targets, self.client.ids[-1])
90 90
91 91 def test_lazy_all_targets(self):
92 92 """test lazy evaluation of rc.direct_view('all')"""
93 93 v = self.client.direct_view()
94 94 self.assertEqual(v.targets, 'all')
95 95
96 96 def double(x):
97 97 return x*2
98 98 seq = range(100)
99 99 ref = [ double(x) for x in seq ]
100 100
101 101 # add some engines, which should be used
102 102 self.add_engines(1)
103 103 n1 = len(self.client.ids)
104 104
105 105 # simple apply
106 106 r = v.apply_sync(lambda : 1)
107 107 self.assertEqual(r, [1] * n1)
108 108
109 109 # map goes through remotefunction
110 110 r = v.map_sync(double, seq)
111 111 self.assertEqual(r, ref)
112 112
113 113 # add a couple more engines, and try again
114 114 self.add_engines(2)
115 115 n2 = len(self.client.ids)
116 116 self.assertNotEqual(n2, n1)
117 117
118 118 # apply
119 119 r = v.apply_sync(lambda : 1)
120 120 self.assertEqual(r, [1] * n2)
121 121
122 122 # map
123 123 r = v.map_sync(double, seq)
124 124 self.assertEqual(r, ref)
125 125
126 126 def test_targets(self):
127 127 """test various valid targets arguments"""
128 128 build = self.client._build_targets
129 129 ids = self.client.ids
130 130 idents,targets = build(None)
131 131 self.assertEqual(ids, targets)
132 132
133 133 def test_clear(self):
134 134 """test clear behavior"""
135 135 self.minimum_engines(2)
136 136 v = self.client[:]
137 137 v.block=True
138 138 v.push(dict(a=5))
139 139 v.pull('a')
140 140 id0 = self.client.ids[-1]
141 141 self.client.clear(targets=id0, block=True)
142 142 a = self.client[:-1].get('a')
143 143 self.assertRaisesRemote(NameError, self.client[id0].get, 'a')
144 144 self.client.clear(block=True)
145 145 for i in self.client.ids:
146 146 self.assertRaisesRemote(NameError, self.client[i].get, 'a')
147 147
148 148 def test_get_result(self):
149 149 """test getting results from the Hub."""
150 150 c = clientmod.Client(profile='iptest')
151 151 t = c.ids[-1]
152 152 ar = c[t].apply_async(wait, 1)
153 153 # give the monitor time to notice the message
154 154 time.sleep(.25)
155 155 ahr = self.client.get_result(ar.msg_ids[0])
156 156 self.assertTrue(isinstance(ahr, AsyncHubResult))
157 157 self.assertEqual(ahr.get(), ar.get())
158 158 ar2 = self.client.get_result(ar.msg_ids[0])
159 159 self.assertFalse(isinstance(ar2, AsyncHubResult))
160 160 c.close()
161 161
162 162 def test_get_execute_result(self):
163 163 """test getting execute results from the Hub."""
164 164 c = clientmod.Client(profile='iptest')
165 165 t = c.ids[-1]
166 166 cell = '\n'.join([
167 167 'import time',
168 168 'time.sleep(0.25)',
169 169 '5'
170 170 ])
171 171 ar = c[t].execute("import time; time.sleep(1)", silent=False)
172 172 # give the monitor time to notice the message
173 173 time.sleep(.25)
174 174 ahr = self.client.get_result(ar.msg_ids[0])
175 175 self.assertTrue(isinstance(ahr, AsyncHubResult))
176 176 self.assertEqual(ahr.get().pyout, ar.get().pyout)
177 177 ar2 = self.client.get_result(ar.msg_ids[0])
178 178 self.assertFalse(isinstance(ar2, AsyncHubResult))
179 179 c.close()
180 180
181 181 def test_ids_list(self):
182 182 """test client.ids"""
183 183 ids = self.client.ids
184 184 self.assertEqual(ids, self.client._ids)
185 185 self.assertFalse(ids is self.client._ids)
186 186 ids.remove(ids[-1])
187 187 self.assertNotEqual(ids, self.client._ids)
188 188
189 189 def test_queue_status(self):
190 190 ids = self.client.ids
191 191 id0 = ids[0]
192 192 qs = self.client.queue_status(targets=id0)
193 193 self.assertTrue(isinstance(qs, dict))
194 194 self.assertEqual(sorted(qs.keys()), ['completed', 'queue', 'tasks'])
195 195 allqs = self.client.queue_status()
196 196 self.assertTrue(isinstance(allqs, dict))
197 197 intkeys = list(allqs.keys())
198 198 intkeys.remove('unassigned')
199 199 self.assertEqual(sorted(intkeys), sorted(self.client.ids))
200 200 unassigned = allqs.pop('unassigned')
201 201 for eid,qs in allqs.items():
202 202 self.assertTrue(isinstance(qs, dict))
203 203 self.assertEqual(sorted(qs.keys()), ['completed', 'queue', 'tasks'])
204 204
205 205 def test_shutdown(self):
206 206 ids = self.client.ids
207 207 id0 = ids[0]
208 208 self.client.shutdown(id0, block=True)
209 209 while id0 in self.client.ids:
210 210 time.sleep(0.1)
211 211 self.client.spin()
212 212
213 213 self.assertRaises(IndexError, lambda : self.client[id0])
214 214
215 215 def test_result_status(self):
216 216 pass
217 217 # to be written
218 218
219 219 def test_db_query_dt(self):
220 220 """test db query by date"""
221 221 hist = self.client.hub_history()
222 222 middle = self.client.db_query({'msg_id' : hist[len(hist)//2]})[0]
223 223 tic = middle['submitted']
224 224 before = self.client.db_query({'submitted' : {'$lt' : tic}})
225 225 after = self.client.db_query({'submitted' : {'$gte' : tic}})
226 226 self.assertEqual(len(before)+len(after),len(hist))
227 227 for b in before:
228 228 self.assertTrue(b['submitted'] < tic)
229 229 for a in after:
230 230 self.assertTrue(a['submitted'] >= tic)
231 231 same = self.client.db_query({'submitted' : tic})
232 232 for s in same:
233 233 self.assertTrue(s['submitted'] == tic)
234 234
235 235 def test_db_query_keys(self):
236 236 """test extracting subset of record keys"""
237 237 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted', 'completed'])
238 238 for rec in found:
239 239 self.assertEqual(set(rec.keys()), set(['msg_id', 'submitted', 'completed']))
240 240
241 241 def test_db_query_default_keys(self):
242 242 """default db_query excludes buffers"""
243 243 found = self.client.db_query({'msg_id': {'$ne' : ''}})
244 244 for rec in found:
245 245 keys = set(rec.keys())
246 246 self.assertFalse('buffers' in keys, "'buffers' should not be in: %s" % keys)
247 247 self.assertFalse('result_buffers' in keys, "'result_buffers' should not be in: %s" % keys)
248 248
249 249 def test_db_query_msg_id(self):
250 250 """ensure msg_id is always in db queries"""
251 251 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted', 'completed'])
252 252 for rec in found:
253 253 self.assertTrue('msg_id' in rec.keys())
254 254 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted'])
255 255 for rec in found:
256 256 self.assertTrue('msg_id' in rec.keys())
257 257 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['msg_id'])
258 258 for rec in found:
259 259 self.assertTrue('msg_id' in rec.keys())
260 260
261 261 def test_db_query_get_result(self):
262 262 """pop in db_query shouldn't pop from result itself"""
263 263 self.client[:].apply_sync(lambda : 1)
264 264 found = self.client.db_query({'msg_id': {'$ne' : ''}})
265 265 rc2 = clientmod.Client(profile='iptest')
266 266 # If this bug is not fixed, this call will hang:
267 267 ar = rc2.get_result(self.client.history[-1])
268 268 ar.wait(2)
269 269 self.assertTrue(ar.ready())
270 270 ar.get()
271 271 rc2.close()
272 272
273 273 def test_db_query_in(self):
274 274 """test db query with '$in','$nin' operators"""
275 275 hist = self.client.hub_history()
276 276 even = hist[::2]
277 277 odd = hist[1::2]
278 278 recs = self.client.db_query({ 'msg_id' : {'$in' : even}})
279 279 found = [ r['msg_id'] for r in recs ]
280 280 self.assertEqual(set(even), set(found))
281 281 recs = self.client.db_query({ 'msg_id' : {'$nin' : even}})
282 282 found = [ r['msg_id'] for r in recs ]
283 283 self.assertEqual(set(odd), set(found))
284 284
285 285 def test_hub_history(self):
286 286 hist = self.client.hub_history()
287 287 recs = self.client.db_query({ 'msg_id' : {"$ne":''}})
288 288 recdict = {}
289 289 for rec in recs:
290 290 recdict[rec['msg_id']] = rec
291 291
292 292 latest = datetime(1984,1,1)
293 293 for msg_id in hist:
294 294 rec = recdict[msg_id]
295 295 newt = rec['submitted']
296 296 self.assertTrue(newt >= latest)
297 297 latest = newt
298 298 ar = self.client[-1].apply_async(lambda : 1)
299 299 ar.get()
300 300 time.sleep(0.25)
301 301 self.assertEqual(self.client.hub_history()[-1:],ar.msg_ids)
302 302
303 303 def _wait_for_idle(self):
304 304 """wait for an engine to become idle, according to the Hub"""
305 305 rc = self.client
306 306
307 307 # step 1. wait for all requests to be noticed
308 308 # timeout 5s, polling every 100ms
309 309 msg_ids = set(rc.history)
310 310 hub_hist = rc.hub_history()
311 311 for i in range(50):
312 312 if msg_ids.difference(hub_hist):
313 313 time.sleep(0.1)
314 314 hub_hist = rc.hub_history()
315 315 else:
316 316 break
317 317
318 318 self.assertEqual(len(msg_ids.difference(hub_hist)), 0)
319 319
320 320 # step 2. wait for all requests to be done
321 321 # timeout 5s, polling every 100ms
322 322 qs = rc.queue_status()
323 323 for i in range(50):
324 324 if qs['unassigned'] or any(qs[eid]['tasks'] for eid in rc.ids):
325 325 time.sleep(0.1)
326 326 qs = rc.queue_status()
327 327 else:
328 328 break
329 329
330 330 # ensure Hub up to date:
331 331 self.assertEqual(qs['unassigned'], 0)
332 332 for eid in rc.ids:
333 333 self.assertEqual(qs[eid]['tasks'], 0)
334 334
335 335
336 336 def test_resubmit(self):
337 337 def f():
338 338 import random
339 339 return random.random()
340 340 v = self.client.load_balanced_view()
341 341 ar = v.apply_async(f)
342 342 r1 = ar.get(1)
343 343 # give the Hub a chance to notice:
344 344 self._wait_for_idle()
345 345 ahr = self.client.resubmit(ar.msg_ids)
346 346 r2 = ahr.get(1)
347 347 self.assertFalse(r1 == r2)
348 348
349 349 def test_resubmit_chain(self):
350 350 """resubmit resubmitted tasks"""
351 351 v = self.client.load_balanced_view()
352 352 ar = v.apply_async(lambda x: x, 'x'*1024)
353 353 ar.get()
354 354 self._wait_for_idle()
355 355 ars = [ar]
356 356
357 357 for i in range(10):
358 358 ar = ars[-1]
359 359 ar2 = self.client.resubmit(ar.msg_ids)
360 360
361 361 [ ar.get() for ar in ars ]
362 362
363 363 def test_resubmit_header(self):
364 364 """resubmit shouldn't clobber the whole header"""
365 365 def f():
366 366 import random
367 367 return random.random()
368 368 v = self.client.load_balanced_view()
369 369 v.retries = 1
370 370 ar = v.apply_async(f)
371 371 r1 = ar.get(1)
372 372 # give the Hub a chance to notice:
373 373 self._wait_for_idle()
374 374 ahr = self.client.resubmit(ar.msg_ids)
375 375 ahr.get(1)
376 376 time.sleep(0.5)
377 377 records = self.client.db_query({'msg_id': {'$in': ar.msg_ids + ahr.msg_ids}}, keys='header')
378 378 h1,h2 = [ r['header'] for r in records ]
379 379 for key in set(h1.keys()).union(set(h2.keys())):
380 380 if key in ('msg_id', 'date'):
381 381 self.assertNotEqual(h1[key], h2[key])
382 382 else:
383 383 self.assertEqual(h1[key], h2[key])
384 384
385 385 def test_resubmit_aborted(self):
386 386 def f():
387 387 import random
388 388 return random.random()
389 389 v = self.client.load_balanced_view()
390 390 # restrict to one engine, so we can put a sleep
391 391 # ahead of the task, so it will get aborted
392 392 eid = self.client.ids[-1]
393 393 v.targets = [eid]
394 394 sleep = v.apply_async(time.sleep, 0.5)
395 395 ar = v.apply_async(f)
396 396 ar.abort()
397 397 self.assertRaises(error.TaskAborted, ar.get)
398 398 # Give the Hub a chance to get up to date:
399 399 self._wait_for_idle()
400 400 ahr = self.client.resubmit(ar.msg_ids)
401 401 r2 = ahr.get(1)
402 402
403 403 def test_resubmit_inflight(self):
404 404 """resubmit of inflight task"""
405 405 v = self.client.load_balanced_view()
406 406 ar = v.apply_async(time.sleep,1)
407 407 # give the message a chance to arrive
408 408 time.sleep(0.2)
409 409 ahr = self.client.resubmit(ar.msg_ids)
410 410 ar.get(2)
411 411 ahr.get(2)
412 412
413 413 def test_resubmit_badkey(self):
414 414 """ensure KeyError on resubmit of nonexistant task"""
415 415 self.assertRaisesRemote(KeyError, self.client.resubmit, ['invalid'])
416 416
417 417 def test_purge_hub_results(self):
418 418 # ensure there are some tasks
419 419 for i in range(5):
420 420 self.client[:].apply_sync(lambda : 1)
421 421 # Wait for the Hub to realise the result is done:
422 422 # This prevents a race condition, where we
423 423 # might purge a result the Hub still thinks is pending.
424 424 self._wait_for_idle()
425 425 rc2 = clientmod.Client(profile='iptest')
426 426 hist = self.client.hub_history()
427 427 ahr = rc2.get_result([hist[-1]])
428 428 ahr.wait(10)
429 429 self.client.purge_hub_results(hist[-1])
430 430 newhist = self.client.hub_history()
431 431 self.assertEqual(len(newhist)+1,len(hist))
432 432 rc2.spin()
433 433 rc2.close()
434 434
435 435 def test_purge_local_results(self):
436 436 # ensure there are some tasks
437 437 res = []
438 438 for i in range(5):
439 439 res.append(self.client[:].apply_async(lambda : 1))
440 440 self._wait_for_idle()
441 441 self.client.wait(10) # wait for the results to come back
442 442 before = len(self.client.results)
443 443 self.assertEqual(len(self.client.metadata),before)
444 444 self.client.purge_local_results(res[-1])
445 445 self.assertEqual(len(self.client.results),before-len(res[-1]), msg="Not removed from results")
446 446 self.assertEqual(len(self.client.metadata),before-len(res[-1]), msg="Not removed from metadata")
447 447
448 448 def test_purge_all_hub_results(self):
449 449 self.client.purge_hub_results('all')
450 450 hist = self.client.hub_history()
451 451 self.assertEqual(len(hist), 0)
452 452
453 453 def test_purge_all_local_results(self):
454 454 self.client.purge_local_results('all')
455 455 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
456 456 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
457 457
458 458 def test_purge_all_results(self):
459 459 # ensure there are some tasks
460 460 for i in range(5):
461 461 self.client[:].apply_sync(lambda : 1)
462 462 self.client.wait(10)
463 463 self._wait_for_idle()
464 464 self.client.purge_results('all')
465 465 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
466 466 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
467 467 hist = self.client.hub_history()
468 468 self.assertEqual(len(hist), 0, msg="hub history not empty")
469 469
470 470 def test_purge_everything(self):
471 471 # ensure there are some tasks
472 472 for i in range(5):
473 473 self.client[:].apply_sync(lambda : 1)
474 474 self.client.wait(10)
475 475 self._wait_for_idle()
476 476 self.client.purge_everything()
477 477 # The client results
478 478 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
479 479 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
480 480 # The client "bookkeeping"
481 481 self.assertEqual(len(self.client.session.digest_history), 0, msg="session digest not empty")
482 482 self.assertEqual(len(self.client.history), 0, msg="client history not empty")
483 483 # the hub results
484 484 hist = self.client.hub_history()
485 485 self.assertEqual(len(hist), 0, msg="hub history not empty")
486 486
487 487
488 488 def test_spin_thread(self):
489 489 self.client.spin_thread(0.01)
490 490 ar = self.client[-1].apply_async(lambda : 1)
491 491 time.sleep(0.1)
492 492 self.assertTrue(ar.wall_time < 0.1,
493 493 "spin should have kept wall_time < 0.1, but got %f" % ar.wall_time
494 494 )
495 495
496 496 def test_stop_spin_thread(self):
497 497 self.client.spin_thread(0.01)
498 498 self.client.stop_spin_thread()
499 499 ar = self.client[-1].apply_async(lambda : 1)
500 500 time.sleep(0.15)
501 501 self.assertTrue(ar.wall_time > 0.1,
502 502 "Shouldn't be spinning, but got wall_time=%f" % ar.wall_time
503 503 )
504 504
505 505 def test_activate(self):
506 506 ip = get_ipython()
507 507 magics = ip.magics_manager.magics
508 508 self.assertTrue('px' in magics['line'])
509 509 self.assertTrue('px' in magics['cell'])
510 510 v0 = self.client.activate(-1, '0')
511 511 self.assertTrue('px0' in magics['line'])
512 512 self.assertTrue('px0' in magics['cell'])
513 513 self.assertEqual(v0.targets, self.client.ids[-1])
514 514 v0 = self.client.activate('all', 'all')
515 515 self.assertTrue('pxall' in magics['line'])
516 516 self.assertTrue('pxall' in magics['cell'])
517 517 self.assertEqual(v0.targets, 'all')
@@ -1,2110 +1,2110 b''
1 1 """ An abstract base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 import os.path
9 9 import re
10 10 import sys
11 11 from textwrap import dedent
12 12 import time
13 13 from unicodedata import category
14 14 import webbrowser
15 15
16 16 # System library imports
17 17 from IPython.external.qt import QtCore, QtGui
18 18
19 19 # Local imports
20 20 from IPython.config.configurable import LoggingConfigurable
21 21 from IPython.core.inputsplitter import ESC_SEQUENCES
22 22 from IPython.qt.rich_text import HtmlExporter
23 23 from IPython.qt.util import MetaQObjectHasTraits, get_font
24 24 from IPython.utils.text import columnize
25 25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
26 from ansi_code_processor import QtAnsiCodeProcessor
27 from completion_widget import CompletionWidget
28 from completion_html import CompletionHtml
29 from completion_plain import CompletionPlain
30 from kill_ring import QtKillRing
26 from .ansi_code_processor import QtAnsiCodeProcessor
27 from .completion_widget import CompletionWidget
28 from .completion_html import CompletionHtml
29 from .completion_plain import CompletionPlain
30 from .kill_ring import QtKillRing
31 31
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Functions
35 35 #-----------------------------------------------------------------------------
36 36
37 37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
38 38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
39 39
40 40 def commonprefix(items):
41 41 """Get common prefix for completions
42 42
43 43 Return the longest common prefix of a list of strings, but with special
44 44 treatment of escape characters that might precede commands in IPython,
45 45 such as %magic functions. Used in tab completion.
46 46
47 47 For a more general function, see os.path.commonprefix
48 48 """
49 49 # the last item will always have the least leading % symbol
50 50 # min / max are first/last in alphabetical order
51 51 first_match = ESCAPE_RE.match(min(items))
52 52 last_match = ESCAPE_RE.match(max(items))
53 53 # common suffix is (common prefix of reversed items) reversed
54 54 if first_match and last_match:
55 55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
56 56 else:
57 57 prefix = ''
58 58
59 59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
60 60 return prefix+os.path.commonprefix(items)
61 61
62 62 def is_letter_or_number(char):
63 63 """ Returns whether the specified unicode character is a letter or a number.
64 64 """
65 65 cat = category(char)
66 66 return cat.startswith('L') or cat.startswith('N')
67 67
68 68 #-----------------------------------------------------------------------------
69 69 # Classes
70 70 #-----------------------------------------------------------------------------
71 71
72 72 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
73 73 """ An abstract base class for console-type widgets. This class has
74 74 functionality for:
75 75
76 76 * Maintaining a prompt and editing region
77 77 * Providing the traditional Unix-style console keyboard shortcuts
78 78 * Performing tab completion
79 79 * Paging text
80 80 * Handling ANSI escape codes
81 81
82 82 ConsoleWidget also provides a number of utility methods that will be
83 83 convenient to implementors of a console-style widget.
84 84 """
85 85 __metaclass__ = MetaQObjectHasTraits
86 86
87 87 #------ Configuration ------------------------------------------------------
88 88
89 89 ansi_codes = Bool(True, config=True,
90 90 help="Whether to process ANSI escape codes."
91 91 )
92 92 buffer_size = Integer(500, config=True,
93 93 help="""
94 94 The maximum number of lines of text before truncation. Specifying a
95 95 non-positive number disables text truncation (not recommended).
96 96 """
97 97 )
98 98 execute_on_complete_input = Bool(True, config=True,
99 99 help="""Whether to automatically execute on syntactically complete input.
100 100
101 101 If False, Shift-Enter is required to submit each execution.
102 102 Disabling this is mainly useful for non-Python kernels,
103 103 where the completion check would be wrong.
104 104 """
105 105 )
106 106 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
107 107 default_value = 'ncurses',
108 108 help="""
109 109 The type of completer to use. Valid values are:
110 110
111 111 'plain' : Show the available completion as a text list
112 112 Below the editing area.
113 113 'droplist': Show the completion in a drop down list navigable
114 114 by the arrow keys, and from which you can select
115 115 completion by pressing Return.
116 116 'ncurses' : Show the completion as a text list which is navigable by
117 117 `tab` and arrow keys.
118 118 """
119 119 )
120 120 # NOTE: this value can only be specified during initialization.
121 121 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
122 122 help="""
123 123 The type of underlying text widget to use. Valid values are 'plain',
124 124 which specifies a QPlainTextEdit, and 'rich', which specifies a
125 125 QTextEdit.
126 126 """
127 127 )
128 128 # NOTE: this value can only be specified during initialization.
129 129 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
130 130 default_value='inside', config=True,
131 131 help="""
132 132 The type of paging to use. Valid values are:
133 133
134 134 'inside' : The widget pages like a traditional terminal.
135 135 'hsplit' : When paging is requested, the widget is split
136 136 horizontally. The top pane contains the console, and the
137 137 bottom pane contains the paged text.
138 138 'vsplit' : Similar to 'hsplit', except that a vertical splitter
139 139 used.
140 140 'custom' : No action is taken by the widget beyond emitting a
141 141 'custom_page_requested(str)' signal.
142 142 'none' : The text is written directly to the console.
143 143 """)
144 144
145 145 font_family = Unicode(config=True,
146 146 help="""The font family to use for the console.
147 147 On OSX this defaults to Monaco, on Windows the default is
148 148 Consolas with fallback of Courier, and on other platforms
149 149 the default is Monospace.
150 150 """)
151 151 def _font_family_default(self):
152 152 if sys.platform == 'win32':
153 153 # Consolas ships with Vista/Win7, fallback to Courier if needed
154 154 return 'Consolas'
155 155 elif sys.platform == 'darwin':
156 156 # OSX always has Monaco, no need for a fallback
157 157 return 'Monaco'
158 158 else:
159 159 # Monospace should always exist, no need for a fallback
160 160 return 'Monospace'
161 161
162 162 font_size = Integer(config=True,
163 163 help="""The font size. If unconfigured, Qt will be entrusted
164 164 with the size of the font.
165 165 """)
166 166
167 167 width = Integer(81, config=True,
168 168 help="""The width of the console at start time in number
169 169 of characters (will double with `hsplit` paging)
170 170 """)
171 171
172 172 height = Integer(25, config=True,
173 173 help="""The height of the console at start time in number
174 174 of characters (will double with `vsplit` paging)
175 175 """)
176 176
177 177 # Whether to override ShortcutEvents for the keybindings defined by this
178 178 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
179 179 # priority (when it has focus) over, e.g., window-level menu shortcuts.
180 180 override_shortcuts = Bool(False)
181 181
182 182 # ------ Custom Qt Widgets -------------------------------------------------
183 183
184 184 # For other projects to easily override the Qt widgets used by the console
185 185 # (e.g. Spyder)
186 186 custom_control = None
187 187 custom_page_control = None
188 188
189 189 #------ Signals ------------------------------------------------------------
190 190
191 191 # Signals that indicate ConsoleWidget state.
192 192 copy_available = QtCore.Signal(bool)
193 193 redo_available = QtCore.Signal(bool)
194 194 undo_available = QtCore.Signal(bool)
195 195
196 196 # Signal emitted when paging is needed and the paging style has been
197 197 # specified as 'custom'.
198 198 custom_page_requested = QtCore.Signal(object)
199 199
200 200 # Signal emitted when the font is changed.
201 201 font_changed = QtCore.Signal(QtGui.QFont)
202 202
203 203 #------ Protected class variables ------------------------------------------
204 204
205 205 # control handles
206 206 _control = None
207 207 _page_control = None
208 208 _splitter = None
209 209
210 210 # When the control key is down, these keys are mapped.
211 211 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
212 212 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
213 213 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
214 214 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
215 215 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
216 216 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
217 217 if not sys.platform == 'darwin':
218 218 # On OS X, Ctrl-E already does the right thing, whereas End moves the
219 219 # cursor to the bottom of the buffer.
220 220 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
221 221
222 222 # The shortcuts defined by this widget. We need to keep track of these to
223 223 # support 'override_shortcuts' above.
224 224 _shortcuts = set(_ctrl_down_remap.keys() +
225 225 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
226 226 QtCore.Qt.Key_V ])
227 227
228 228 _temp_buffer_filled = False
229 229
230 230 #---------------------------------------------------------------------------
231 231 # 'QObject' interface
232 232 #---------------------------------------------------------------------------
233 233
234 234 def __init__(self, parent=None, **kw):
235 235 """ Create a ConsoleWidget.
236 236
237 237 Parameters:
238 238 -----------
239 239 parent : QWidget, optional [default None]
240 240 The parent for this widget.
241 241 """
242 242 QtGui.QWidget.__init__(self, parent)
243 243 LoggingConfigurable.__init__(self, **kw)
244 244
245 245 # While scrolling the pager on Mac OS X, it tears badly. The
246 246 # NativeGesture is platform and perhaps build-specific hence
247 247 # we take adequate precautions here.
248 248 self._pager_scroll_events = [QtCore.QEvent.Wheel]
249 249 if hasattr(QtCore.QEvent, 'NativeGesture'):
250 250 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
251 251
252 252 # Create the layout and underlying text widget.
253 253 layout = QtGui.QStackedLayout(self)
254 254 layout.setContentsMargins(0, 0, 0, 0)
255 255 self._control = self._create_control()
256 256 if self.paging in ('hsplit', 'vsplit'):
257 257 self._splitter = QtGui.QSplitter()
258 258 if self.paging == 'hsplit':
259 259 self._splitter.setOrientation(QtCore.Qt.Horizontal)
260 260 else:
261 261 self._splitter.setOrientation(QtCore.Qt.Vertical)
262 262 self._splitter.addWidget(self._control)
263 263 layout.addWidget(self._splitter)
264 264 else:
265 265 layout.addWidget(self._control)
266 266
267 267 # Create the paging widget, if necessary.
268 268 if self.paging in ('inside', 'hsplit', 'vsplit'):
269 269 self._page_control = self._create_page_control()
270 270 if self._splitter:
271 271 self._page_control.hide()
272 272 self._splitter.addWidget(self._page_control)
273 273 else:
274 274 layout.addWidget(self._page_control)
275 275
276 276 # Initialize protected variables. Some variables contain useful state
277 277 # information for subclasses; they should be considered read-only.
278 278 self._append_before_prompt_pos = 0
279 279 self._ansi_processor = QtAnsiCodeProcessor()
280 280 if self.gui_completion == 'ncurses':
281 281 self._completion_widget = CompletionHtml(self)
282 282 elif self.gui_completion == 'droplist':
283 283 self._completion_widget = CompletionWidget(self)
284 284 elif self.gui_completion == 'plain':
285 285 self._completion_widget = CompletionPlain(self)
286 286
287 287 self._continuation_prompt = '> '
288 288 self._continuation_prompt_html = None
289 289 self._executing = False
290 290 self._filter_resize = False
291 291 self._html_exporter = HtmlExporter(self._control)
292 292 self._input_buffer_executing = ''
293 293 self._input_buffer_pending = ''
294 294 self._kill_ring = QtKillRing(self._control)
295 295 self._prompt = ''
296 296 self._prompt_html = None
297 297 self._prompt_pos = 0
298 298 self._prompt_sep = ''
299 299 self._reading = False
300 300 self._reading_callback = None
301 301 self._tab_width = 8
302 302
303 303 # List of strings pending to be appended as plain text in the widget.
304 304 # The text is not immediately inserted when available to not
305 305 # choke the Qt event loop with paint events for the widget in
306 306 # case of lots of output from kernel.
307 307 self._pending_insert_text = []
308 308
309 309 # Timer to flush the pending stream messages. The interval is adjusted
310 310 # later based on actual time taken for flushing a screen (buffer_size)
311 311 # of output text.
312 312 self._pending_text_flush_interval = QtCore.QTimer(self._control)
313 313 self._pending_text_flush_interval.setInterval(100)
314 314 self._pending_text_flush_interval.setSingleShot(True)
315 315 self._pending_text_flush_interval.timeout.connect(
316 316 self._flush_pending_stream)
317 317
318 318 # Set a monospaced font.
319 319 self.reset_font()
320 320
321 321 # Configure actions.
322 322 action = QtGui.QAction('Print', None)
323 323 action.setEnabled(True)
324 324 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
325 325 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
326 326 # Only override the default if there is a collision.
327 327 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
328 328 printkey = "Ctrl+Shift+P"
329 329 action.setShortcut(printkey)
330 330 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
331 331 action.triggered.connect(self.print_)
332 332 self.addAction(action)
333 333 self.print_action = action
334 334
335 335 action = QtGui.QAction('Save as HTML/XML', None)
336 336 action.setShortcut(QtGui.QKeySequence.Save)
337 337 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
338 338 action.triggered.connect(self.export_html)
339 339 self.addAction(action)
340 340 self.export_action = action
341 341
342 342 action = QtGui.QAction('Select All', None)
343 343 action.setEnabled(True)
344 344 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
345 345 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
346 346 # Only override the default if there is a collision.
347 347 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
348 348 selectall = "Ctrl+Shift+A"
349 349 action.setShortcut(selectall)
350 350 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
351 351 action.triggered.connect(self.select_all)
352 352 self.addAction(action)
353 353 self.select_all_action = action
354 354
355 355 self.increase_font_size = QtGui.QAction("Bigger Font",
356 356 self,
357 357 shortcut=QtGui.QKeySequence.ZoomIn,
358 358 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
359 359 statusTip="Increase the font size by one point",
360 360 triggered=self._increase_font_size)
361 361 self.addAction(self.increase_font_size)
362 362
363 363 self.decrease_font_size = QtGui.QAction("Smaller Font",
364 364 self,
365 365 shortcut=QtGui.QKeySequence.ZoomOut,
366 366 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
367 367 statusTip="Decrease the font size by one point",
368 368 triggered=self._decrease_font_size)
369 369 self.addAction(self.decrease_font_size)
370 370
371 371 self.reset_font_size = QtGui.QAction("Normal Font",
372 372 self,
373 373 shortcut="Ctrl+0",
374 374 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
375 375 statusTip="Restore the Normal font size",
376 376 triggered=self.reset_font)
377 377 self.addAction(self.reset_font_size)
378 378
379 379 # Accept drag and drop events here. Drops were already turned off
380 380 # in self._control when that widget was created.
381 381 self.setAcceptDrops(True)
382 382
383 383 #---------------------------------------------------------------------------
384 384 # Drag and drop support
385 385 #---------------------------------------------------------------------------
386 386
387 387 def dragEnterEvent(self, e):
388 388 if e.mimeData().hasUrls():
389 389 # The link action should indicate to that the drop will insert
390 390 # the file anme.
391 391 e.setDropAction(QtCore.Qt.LinkAction)
392 392 e.accept()
393 393 elif e.mimeData().hasText():
394 394 # By changing the action to copy we don't need to worry about
395 395 # the user accidentally moving text around in the widget.
396 396 e.setDropAction(QtCore.Qt.CopyAction)
397 397 e.accept()
398 398
399 399 def dragMoveEvent(self, e):
400 400 if e.mimeData().hasUrls():
401 401 pass
402 402 elif e.mimeData().hasText():
403 403 cursor = self._control.cursorForPosition(e.pos())
404 404 if self._in_buffer(cursor.position()):
405 405 e.setDropAction(QtCore.Qt.CopyAction)
406 406 self._control.setTextCursor(cursor)
407 407 else:
408 408 e.setDropAction(QtCore.Qt.IgnoreAction)
409 409 e.accept()
410 410
411 411 def dropEvent(self, e):
412 412 if e.mimeData().hasUrls():
413 413 self._keep_cursor_in_buffer()
414 414 cursor = self._control.textCursor()
415 415 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
416 416 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
417 417 for f in filenames)
418 418 self._insert_plain_text_into_buffer(cursor, text)
419 419 elif e.mimeData().hasText():
420 420 cursor = self._control.cursorForPosition(e.pos())
421 421 if self._in_buffer(cursor.position()):
422 422 text = e.mimeData().text()
423 423 self._insert_plain_text_into_buffer(cursor, text)
424 424
425 425 def eventFilter(self, obj, event):
426 426 """ Reimplemented to ensure a console-like behavior in the underlying
427 427 text widgets.
428 428 """
429 429 etype = event.type()
430 430 if etype == QtCore.QEvent.KeyPress:
431 431
432 432 # Re-map keys for all filtered widgets.
433 433 key = event.key()
434 434 if self._control_key_down(event.modifiers()) and \
435 435 key in self._ctrl_down_remap:
436 436 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
437 437 self._ctrl_down_remap[key],
438 438 QtCore.Qt.NoModifier)
439 439 QtGui.qApp.sendEvent(obj, new_event)
440 440 return True
441 441
442 442 elif obj == self._control:
443 443 return self._event_filter_console_keypress(event)
444 444
445 445 elif obj == self._page_control:
446 446 return self._event_filter_page_keypress(event)
447 447
448 448 # Make middle-click paste safe.
449 449 elif etype == QtCore.QEvent.MouseButtonRelease and \
450 450 event.button() == QtCore.Qt.MidButton and \
451 451 obj == self._control.viewport():
452 452 cursor = self._control.cursorForPosition(event.pos())
453 453 self._control.setTextCursor(cursor)
454 454 self.paste(QtGui.QClipboard.Selection)
455 455 return True
456 456
457 457 # Manually adjust the scrollbars *after* a resize event is dispatched.
458 458 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
459 459 self._filter_resize = True
460 460 QtGui.qApp.sendEvent(obj, event)
461 461 self._adjust_scrollbars()
462 462 self._filter_resize = False
463 463 return True
464 464
465 465 # Override shortcuts for all filtered widgets.
466 466 elif etype == QtCore.QEvent.ShortcutOverride and \
467 467 self.override_shortcuts and \
468 468 self._control_key_down(event.modifiers()) and \
469 469 event.key() in self._shortcuts:
470 470 event.accept()
471 471
472 472 # Handle scrolling of the vsplit pager. This hack attempts to solve
473 473 # problems with tearing of the help text inside the pager window. This
474 474 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
475 475 # perfect but makes the pager more usable.
476 476 elif etype in self._pager_scroll_events and \
477 477 obj == self._page_control:
478 478 self._page_control.repaint()
479 479 return True
480 480
481 481 elif etype == QtCore.QEvent.MouseMove:
482 482 anchor = self._control.anchorAt(event.pos())
483 483 QtGui.QToolTip.showText(event.globalPos(), anchor)
484 484
485 485 return super(ConsoleWidget, self).eventFilter(obj, event)
486 486
487 487 #---------------------------------------------------------------------------
488 488 # 'QWidget' interface
489 489 #---------------------------------------------------------------------------
490 490
491 491 def sizeHint(self):
492 492 """ Reimplemented to suggest a size that is 80 characters wide and
493 493 25 lines high.
494 494 """
495 495 font_metrics = QtGui.QFontMetrics(self.font)
496 496 margin = (self._control.frameWidth() +
497 497 self._control.document().documentMargin()) * 2
498 498 style = self.style()
499 499 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
500 500
501 501 # Note 1: Despite my best efforts to take the various margins into
502 502 # account, the width is still coming out a bit too small, so we include
503 503 # a fudge factor of one character here.
504 504 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
505 505 # to a Qt bug on certain Mac OS systems where it returns 0.
506 506 width = font_metrics.width(' ') * self.width + margin
507 507 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
508 508 if self.paging == 'hsplit':
509 509 width = width * 2 + splitwidth
510 510
511 511 height = font_metrics.height() * self.height + margin
512 512 if self.paging == 'vsplit':
513 513 height = height * 2 + splitwidth
514 514
515 515 return QtCore.QSize(width, height)
516 516
517 517 #---------------------------------------------------------------------------
518 518 # 'ConsoleWidget' public interface
519 519 #---------------------------------------------------------------------------
520 520
521 521 def can_copy(self):
522 522 """ Returns whether text can be copied to the clipboard.
523 523 """
524 524 return self._control.textCursor().hasSelection()
525 525
526 526 def can_cut(self):
527 527 """ Returns whether text can be cut to the clipboard.
528 528 """
529 529 cursor = self._control.textCursor()
530 530 return (cursor.hasSelection() and
531 531 self._in_buffer(cursor.anchor()) and
532 532 self._in_buffer(cursor.position()))
533 533
534 534 def can_paste(self):
535 535 """ Returns whether text can be pasted from the clipboard.
536 536 """
537 537 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
538 538 return bool(QtGui.QApplication.clipboard().text())
539 539 return False
540 540
541 541 def clear(self, keep_input=True):
542 542 """ Clear the console.
543 543
544 544 Parameters:
545 545 -----------
546 546 keep_input : bool, optional (default True)
547 547 If set, restores the old input buffer if a new prompt is written.
548 548 """
549 549 if self._executing:
550 550 self._control.clear()
551 551 else:
552 552 if keep_input:
553 553 input_buffer = self.input_buffer
554 554 self._control.clear()
555 555 self._show_prompt()
556 556 if keep_input:
557 557 self.input_buffer = input_buffer
558 558
559 559 def copy(self):
560 560 """ Copy the currently selected text to the clipboard.
561 561 """
562 562 self.layout().currentWidget().copy()
563 563
564 564 def copy_anchor(self, anchor):
565 565 """ Copy anchor text to the clipboard
566 566 """
567 567 QtGui.QApplication.clipboard().setText(anchor)
568 568
569 569 def cut(self):
570 570 """ Copy the currently selected text to the clipboard and delete it
571 571 if it's inside the input buffer.
572 572 """
573 573 self.copy()
574 574 if self.can_cut():
575 575 self._control.textCursor().removeSelectedText()
576 576
577 577 def execute(self, source=None, hidden=False, interactive=False):
578 578 """ Executes source or the input buffer, possibly prompting for more
579 579 input.
580 580
581 581 Parameters:
582 582 -----------
583 583 source : str, optional
584 584
585 585 The source to execute. If not specified, the input buffer will be
586 586 used. If specified and 'hidden' is False, the input buffer will be
587 587 replaced with the source before execution.
588 588
589 589 hidden : bool, optional (default False)
590 590
591 591 If set, no output will be shown and the prompt will not be modified.
592 592 In other words, it will be completely invisible to the user that
593 593 an execution has occurred.
594 594
595 595 interactive : bool, optional (default False)
596 596
597 597 Whether the console is to treat the source as having been manually
598 598 entered by the user. The effect of this parameter depends on the
599 599 subclass implementation.
600 600
601 601 Raises:
602 602 -------
603 603 RuntimeError
604 604 If incomplete input is given and 'hidden' is True. In this case,
605 605 it is not possible to prompt for more input.
606 606
607 607 Returns:
608 608 --------
609 609 A boolean indicating whether the source was executed.
610 610 """
611 611 # WARNING: The order in which things happen here is very particular, in
612 612 # large part because our syntax highlighting is fragile. If you change
613 613 # something, test carefully!
614 614
615 615 # Decide what to execute.
616 616 if source is None:
617 617 source = self.input_buffer
618 618 if not hidden:
619 619 # A newline is appended later, but it should be considered part
620 620 # of the input buffer.
621 621 source += '\n'
622 622 elif not hidden:
623 623 self.input_buffer = source
624 624
625 625 # Execute the source or show a continuation prompt if it is incomplete.
626 626 if self.execute_on_complete_input:
627 627 complete = self._is_complete(source, interactive)
628 628 else:
629 629 complete = not interactive
630 630 if hidden:
631 631 if complete or not self.execute_on_complete_input:
632 632 self._execute(source, hidden)
633 633 else:
634 634 error = 'Incomplete noninteractive input: "%s"'
635 635 raise RuntimeError(error % source)
636 636 else:
637 637 if complete:
638 638 self._append_plain_text('\n')
639 639 self._input_buffer_executing = self.input_buffer
640 640 self._executing = True
641 641 self._prompt_finished()
642 642
643 643 # The maximum block count is only in effect during execution.
644 644 # This ensures that _prompt_pos does not become invalid due to
645 645 # text truncation.
646 646 self._control.document().setMaximumBlockCount(self.buffer_size)
647 647
648 648 # Setting a positive maximum block count will automatically
649 649 # disable the undo/redo history, but just to be safe:
650 650 self._control.setUndoRedoEnabled(False)
651 651
652 652 # Perform actual execution.
653 653 self._execute(source, hidden)
654 654
655 655 else:
656 656 # Do this inside an edit block so continuation prompts are
657 657 # removed seamlessly via undo/redo.
658 658 cursor = self._get_end_cursor()
659 659 cursor.beginEditBlock()
660 660 cursor.insertText('\n')
661 661 self._insert_continuation_prompt(cursor)
662 662 cursor.endEditBlock()
663 663
664 664 # Do not do this inside the edit block. It works as expected
665 665 # when using a QPlainTextEdit control, but does not have an
666 666 # effect when using a QTextEdit. I believe this is a Qt bug.
667 667 self._control.moveCursor(QtGui.QTextCursor.End)
668 668
669 669 return complete
670 670
671 671 def export_html(self):
672 672 """ Shows a dialog to export HTML/XML in various formats.
673 673 """
674 674 self._html_exporter.export()
675 675
676 676 def _get_input_buffer(self, force=False):
677 677 """ The text that the user has entered entered at the current prompt.
678 678
679 679 If the console is currently executing, the text that is executing will
680 680 always be returned.
681 681 """
682 682 # If we're executing, the input buffer may not even exist anymore due to
683 683 # the limit imposed by 'buffer_size'. Therefore, we store it.
684 684 if self._executing and not force:
685 685 return self._input_buffer_executing
686 686
687 687 cursor = self._get_end_cursor()
688 688 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
689 689 input_buffer = cursor.selection().toPlainText()
690 690
691 691 # Strip out continuation prompts.
692 692 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
693 693
694 694 def _set_input_buffer(self, string):
695 695 """ Sets the text in the input buffer.
696 696
697 697 If the console is currently executing, this call has no *immediate*
698 698 effect. When the execution is finished, the input buffer will be updated
699 699 appropriately.
700 700 """
701 701 # If we're executing, store the text for later.
702 702 if self._executing:
703 703 self._input_buffer_pending = string
704 704 return
705 705
706 706 # Remove old text.
707 707 cursor = self._get_end_cursor()
708 708 cursor.beginEditBlock()
709 709 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
710 710 cursor.removeSelectedText()
711 711
712 712 # Insert new text with continuation prompts.
713 713 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
714 714 cursor.endEditBlock()
715 715 self._control.moveCursor(QtGui.QTextCursor.End)
716 716
717 717 input_buffer = property(_get_input_buffer, _set_input_buffer)
718 718
719 719 def _get_font(self):
720 720 """ The base font being used by the ConsoleWidget.
721 721 """
722 722 return self._control.document().defaultFont()
723 723
724 724 def _set_font(self, font):
725 725 """ Sets the base font for the ConsoleWidget to the specified QFont.
726 726 """
727 727 font_metrics = QtGui.QFontMetrics(font)
728 728 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
729 729
730 730 self._completion_widget.setFont(font)
731 731 self._control.document().setDefaultFont(font)
732 732 if self._page_control:
733 733 self._page_control.document().setDefaultFont(font)
734 734
735 735 self.font_changed.emit(font)
736 736
737 737 font = property(_get_font, _set_font)
738 738
739 739 def open_anchor(self, anchor):
740 740 """ Open selected anchor in the default webbrowser
741 741 """
742 742 webbrowser.open( anchor )
743 743
744 744 def paste(self, mode=QtGui.QClipboard.Clipboard):
745 745 """ Paste the contents of the clipboard into the input region.
746 746
747 747 Parameters:
748 748 -----------
749 749 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
750 750
751 751 Controls which part of the system clipboard is used. This can be
752 752 used to access the selection clipboard in X11 and the Find buffer
753 753 in Mac OS. By default, the regular clipboard is used.
754 754 """
755 755 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
756 756 # Make sure the paste is safe.
757 757 self._keep_cursor_in_buffer()
758 758 cursor = self._control.textCursor()
759 759
760 760 # Remove any trailing newline, which confuses the GUI and forces the
761 761 # user to backspace.
762 762 text = QtGui.QApplication.clipboard().text(mode).rstrip()
763 763 self._insert_plain_text_into_buffer(cursor, dedent(text))
764 764
765 765 def print_(self, printer = None):
766 766 """ Print the contents of the ConsoleWidget to the specified QPrinter.
767 767 """
768 768 if (not printer):
769 769 printer = QtGui.QPrinter()
770 770 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
771 771 return
772 772 self._control.print_(printer)
773 773
774 774 def prompt_to_top(self):
775 775 """ Moves the prompt to the top of the viewport.
776 776 """
777 777 if not self._executing:
778 778 prompt_cursor = self._get_prompt_cursor()
779 779 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
780 780 self._set_cursor(prompt_cursor)
781 781 self._set_top_cursor(prompt_cursor)
782 782
783 783 def redo(self):
784 784 """ Redo the last operation. If there is no operation to redo, nothing
785 785 happens.
786 786 """
787 787 self._control.redo()
788 788
789 789 def reset_font(self):
790 790 """ Sets the font to the default fixed-width font for this platform.
791 791 """
792 792 if sys.platform == 'win32':
793 793 # Consolas ships with Vista/Win7, fallback to Courier if needed
794 794 fallback = 'Courier'
795 795 elif sys.platform == 'darwin':
796 796 # OSX always has Monaco
797 797 fallback = 'Monaco'
798 798 else:
799 799 # Monospace should always exist
800 800 fallback = 'Monospace'
801 801 font = get_font(self.font_family, fallback)
802 802 if self.font_size:
803 803 font.setPointSize(self.font_size)
804 804 else:
805 805 font.setPointSize(QtGui.qApp.font().pointSize())
806 806 font.setStyleHint(QtGui.QFont.TypeWriter)
807 807 self._set_font(font)
808 808
809 809 def change_font_size(self, delta):
810 810 """Change the font size by the specified amount (in points).
811 811 """
812 812 font = self.font
813 813 size = max(font.pointSize() + delta, 1) # minimum 1 point
814 814 font.setPointSize(size)
815 815 self._set_font(font)
816 816
817 817 def _increase_font_size(self):
818 818 self.change_font_size(1)
819 819
820 820 def _decrease_font_size(self):
821 821 self.change_font_size(-1)
822 822
823 823 def select_all(self):
824 824 """ Selects all the text in the buffer.
825 825 """
826 826 self._control.selectAll()
827 827
828 828 def _get_tab_width(self):
829 829 """ The width (in terms of space characters) for tab characters.
830 830 """
831 831 return self._tab_width
832 832
833 833 def _set_tab_width(self, tab_width):
834 834 """ Sets the width (in terms of space characters) for tab characters.
835 835 """
836 836 font_metrics = QtGui.QFontMetrics(self.font)
837 837 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
838 838
839 839 self._tab_width = tab_width
840 840
841 841 tab_width = property(_get_tab_width, _set_tab_width)
842 842
843 843 def undo(self):
844 844 """ Undo the last operation. If there is no operation to undo, nothing
845 845 happens.
846 846 """
847 847 self._control.undo()
848 848
849 849 #---------------------------------------------------------------------------
850 850 # 'ConsoleWidget' abstract interface
851 851 #---------------------------------------------------------------------------
852 852
853 853 def _is_complete(self, source, interactive):
854 854 """ Returns whether 'source' can be executed. When triggered by an
855 855 Enter/Return key press, 'interactive' is True; otherwise, it is
856 856 False.
857 857 """
858 858 raise NotImplementedError
859 859
860 860 def _execute(self, source, hidden):
861 861 """ Execute 'source'. If 'hidden', do not show any output.
862 862 """
863 863 raise NotImplementedError
864 864
865 865 def _prompt_started_hook(self):
866 866 """ Called immediately after a new prompt is displayed.
867 867 """
868 868 pass
869 869
870 870 def _prompt_finished_hook(self):
871 871 """ Called immediately after a prompt is finished, i.e. when some input
872 872 will be processed and a new prompt displayed.
873 873 """
874 874 pass
875 875
876 876 def _up_pressed(self, shift_modifier):
877 877 """ Called when the up key is pressed. Returns whether to continue
878 878 processing the event.
879 879 """
880 880 return True
881 881
882 882 def _down_pressed(self, shift_modifier):
883 883 """ Called when the down key is pressed. Returns whether to continue
884 884 processing the event.
885 885 """
886 886 return True
887 887
888 888 def _tab_pressed(self):
889 889 """ Called when the tab key is pressed. Returns whether to continue
890 890 processing the event.
891 891 """
892 892 return False
893 893
894 894 #--------------------------------------------------------------------------
895 895 # 'ConsoleWidget' protected interface
896 896 #--------------------------------------------------------------------------
897 897
898 898 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
899 899 """ A low-level method for appending content to the end of the buffer.
900 900
901 901 If 'before_prompt' is enabled, the content will be inserted before the
902 902 current prompt, if there is one.
903 903 """
904 904 # Determine where to insert the content.
905 905 cursor = self._control.textCursor()
906 906 if before_prompt and (self._reading or not self._executing):
907 907 self._flush_pending_stream()
908 908 cursor.setPosition(self._append_before_prompt_pos)
909 909 else:
910 910 if insert != self._insert_plain_text:
911 911 self._flush_pending_stream()
912 912 cursor.movePosition(QtGui.QTextCursor.End)
913 913 start_pos = cursor.position()
914 914
915 915 # Perform the insertion.
916 916 result = insert(cursor, input, *args, **kwargs)
917 917
918 918 # Adjust the prompt position if we have inserted before it. This is safe
919 919 # because buffer truncation is disabled when not executing.
920 920 if before_prompt and not self._executing:
921 921 diff = cursor.position() - start_pos
922 922 self._append_before_prompt_pos += diff
923 923 self._prompt_pos += diff
924 924
925 925 return result
926 926
927 927 def _append_block(self, block_format=None, before_prompt=False):
928 928 """ Appends an new QTextBlock to the end of the console buffer.
929 929 """
930 930 self._append_custom(self._insert_block, block_format, before_prompt)
931 931
932 932 def _append_html(self, html, before_prompt=False):
933 933 """ Appends HTML at the end of the console buffer.
934 934 """
935 935 self._append_custom(self._insert_html, html, before_prompt)
936 936
937 937 def _append_html_fetching_plain_text(self, html, before_prompt=False):
938 938 """ Appends HTML, then returns the plain text version of it.
939 939 """
940 940 return self._append_custom(self._insert_html_fetching_plain_text,
941 941 html, before_prompt)
942 942
943 943 def _append_plain_text(self, text, before_prompt=False):
944 944 """ Appends plain text, processing ANSI codes if enabled.
945 945 """
946 946 self._append_custom(self._insert_plain_text, text, before_prompt)
947 947
948 948 def _cancel_completion(self):
949 949 """ If text completion is progress, cancel it.
950 950 """
951 951 self._completion_widget.cancel_completion()
952 952
953 953 def _clear_temporary_buffer(self):
954 954 """ Clears the "temporary text" buffer, i.e. all the text following
955 955 the prompt region.
956 956 """
957 957 # Select and remove all text below the input buffer.
958 958 cursor = self._get_prompt_cursor()
959 959 prompt = self._continuation_prompt.lstrip()
960 960 if(self._temp_buffer_filled):
961 961 self._temp_buffer_filled = False
962 962 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
963 963 temp_cursor = QtGui.QTextCursor(cursor)
964 964 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
965 965 text = temp_cursor.selection().toPlainText().lstrip()
966 966 if not text.startswith(prompt):
967 967 break
968 968 else:
969 969 # We've reached the end of the input buffer and no text follows.
970 970 return
971 971 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
972 972 cursor.movePosition(QtGui.QTextCursor.End,
973 973 QtGui.QTextCursor.KeepAnchor)
974 974 cursor.removeSelectedText()
975 975
976 976 # After doing this, we have no choice but to clear the undo/redo
977 977 # history. Otherwise, the text is not "temporary" at all, because it
978 978 # can be recalled with undo/redo. Unfortunately, Qt does not expose
979 979 # fine-grained control to the undo/redo system.
980 980 if self._control.isUndoRedoEnabled():
981 981 self._control.setUndoRedoEnabled(False)
982 982 self._control.setUndoRedoEnabled(True)
983 983
984 984 def _complete_with_items(self, cursor, items):
985 985 """ Performs completion with 'items' at the specified cursor location.
986 986 """
987 987 self._cancel_completion()
988 988
989 989 if len(items) == 1:
990 990 cursor.setPosition(self._control.textCursor().position(),
991 991 QtGui.QTextCursor.KeepAnchor)
992 992 cursor.insertText(items[0])
993 993
994 994 elif len(items) > 1:
995 995 current_pos = self._control.textCursor().position()
996 996 prefix = commonprefix(items)
997 997 if prefix:
998 998 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
999 999 cursor.insertText(prefix)
1000 1000 current_pos = cursor.position()
1001 1001
1002 1002 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1003 1003 self._completion_widget.show_items(cursor, items)
1004 1004
1005 1005
1006 1006 def _fill_temporary_buffer(self, cursor, text, html=False):
1007 1007 """fill the area below the active editting zone with text"""
1008 1008
1009 1009 current_pos = self._control.textCursor().position()
1010 1010
1011 1011 cursor.beginEditBlock()
1012 1012 self._append_plain_text('\n')
1013 1013 self._page(text, html=html)
1014 1014 cursor.endEditBlock()
1015 1015
1016 1016 cursor.setPosition(current_pos)
1017 1017 self._control.moveCursor(QtGui.QTextCursor.End)
1018 1018 self._control.setTextCursor(cursor)
1019 1019
1020 1020 self._temp_buffer_filled = True
1021 1021
1022 1022
1023 1023 def _context_menu_make(self, pos):
1024 1024 """ Creates a context menu for the given QPoint (in widget coordinates).
1025 1025 """
1026 1026 menu = QtGui.QMenu(self)
1027 1027
1028 1028 self.cut_action = menu.addAction('Cut', self.cut)
1029 1029 self.cut_action.setEnabled(self.can_cut())
1030 1030 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1031 1031
1032 1032 self.copy_action = menu.addAction('Copy', self.copy)
1033 1033 self.copy_action.setEnabled(self.can_copy())
1034 1034 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1035 1035
1036 1036 self.paste_action = menu.addAction('Paste', self.paste)
1037 1037 self.paste_action.setEnabled(self.can_paste())
1038 1038 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1039 1039
1040 1040 anchor = self._control.anchorAt(pos)
1041 1041 if anchor:
1042 1042 menu.addSeparator()
1043 1043 self.copy_link_action = menu.addAction(
1044 1044 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1045 1045 self.open_link_action = menu.addAction(
1046 1046 'Open Link', lambda: self.open_anchor(anchor=anchor))
1047 1047
1048 1048 menu.addSeparator()
1049 1049 menu.addAction(self.select_all_action)
1050 1050
1051 1051 menu.addSeparator()
1052 1052 menu.addAction(self.export_action)
1053 1053 menu.addAction(self.print_action)
1054 1054
1055 1055 return menu
1056 1056
1057 1057 def _control_key_down(self, modifiers, include_command=False):
1058 1058 """ Given a KeyboardModifiers flags object, return whether the Control
1059 1059 key is down.
1060 1060
1061 1061 Parameters:
1062 1062 -----------
1063 1063 include_command : bool, optional (default True)
1064 1064 Whether to treat the Command key as a (mutually exclusive) synonym
1065 1065 for Control when in Mac OS.
1066 1066 """
1067 1067 # Note that on Mac OS, ControlModifier corresponds to the Command key
1068 1068 # while MetaModifier corresponds to the Control key.
1069 1069 if sys.platform == 'darwin':
1070 1070 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1071 1071 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1072 1072 else:
1073 1073 return bool(modifiers & QtCore.Qt.ControlModifier)
1074 1074
1075 1075 def _create_control(self):
1076 1076 """ Creates and connects the underlying text widget.
1077 1077 """
1078 1078 # Create the underlying control.
1079 1079 if self.custom_control:
1080 1080 control = self.custom_control()
1081 1081 elif self.kind == 'plain':
1082 1082 control = QtGui.QPlainTextEdit()
1083 1083 elif self.kind == 'rich':
1084 1084 control = QtGui.QTextEdit()
1085 1085 control.setAcceptRichText(False)
1086 1086 control.setMouseTracking(True)
1087 1087
1088 1088 # Prevent the widget from handling drops, as we already provide
1089 1089 # the logic in this class.
1090 1090 control.setAcceptDrops(False)
1091 1091
1092 1092 # Install event filters. The filter on the viewport is needed for
1093 1093 # mouse events.
1094 1094 control.installEventFilter(self)
1095 1095 control.viewport().installEventFilter(self)
1096 1096
1097 1097 # Connect signals.
1098 1098 control.customContextMenuRequested.connect(
1099 1099 self._custom_context_menu_requested)
1100 1100 control.copyAvailable.connect(self.copy_available)
1101 1101 control.redoAvailable.connect(self.redo_available)
1102 1102 control.undoAvailable.connect(self.undo_available)
1103 1103
1104 1104 # Hijack the document size change signal to prevent Qt from adjusting
1105 1105 # the viewport's scrollbar. We are relying on an implementation detail
1106 1106 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1107 1107 # this functionality we cannot create a nice terminal interface.
1108 1108 layout = control.document().documentLayout()
1109 1109 layout.documentSizeChanged.disconnect()
1110 1110 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1111 1111
1112 1112 # Configure the control.
1113 1113 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1114 1114 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1115 1115 control.setReadOnly(True)
1116 1116 control.setUndoRedoEnabled(False)
1117 1117 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1118 1118 return control
1119 1119
1120 1120 def _create_page_control(self):
1121 1121 """ Creates and connects the underlying paging widget.
1122 1122 """
1123 1123 if self.custom_page_control:
1124 1124 control = self.custom_page_control()
1125 1125 elif self.kind == 'plain':
1126 1126 control = QtGui.QPlainTextEdit()
1127 1127 elif self.kind == 'rich':
1128 1128 control = QtGui.QTextEdit()
1129 1129 control.installEventFilter(self)
1130 1130 viewport = control.viewport()
1131 1131 viewport.installEventFilter(self)
1132 1132 control.setReadOnly(True)
1133 1133 control.setUndoRedoEnabled(False)
1134 1134 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1135 1135 return control
1136 1136
1137 1137 def _event_filter_console_keypress(self, event):
1138 1138 """ Filter key events for the underlying text widget to create a
1139 1139 console-like interface.
1140 1140 """
1141 1141 intercepted = False
1142 1142 cursor = self._control.textCursor()
1143 1143 position = cursor.position()
1144 1144 key = event.key()
1145 1145 ctrl_down = self._control_key_down(event.modifiers())
1146 1146 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1147 1147 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1148 1148
1149 1149 #------ Special sequences ----------------------------------------------
1150 1150
1151 1151 if event.matches(QtGui.QKeySequence.Copy):
1152 1152 self.copy()
1153 1153 intercepted = True
1154 1154
1155 1155 elif event.matches(QtGui.QKeySequence.Cut):
1156 1156 self.cut()
1157 1157 intercepted = True
1158 1158
1159 1159 elif event.matches(QtGui.QKeySequence.Paste):
1160 1160 self.paste()
1161 1161 intercepted = True
1162 1162
1163 1163 #------ Special modifier logic -----------------------------------------
1164 1164
1165 1165 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1166 1166 intercepted = True
1167 1167
1168 1168 # Special handling when tab completing in text mode.
1169 1169 self._cancel_completion()
1170 1170
1171 1171 if self._in_buffer(position):
1172 1172 # Special handling when a reading a line of raw input.
1173 1173 if self._reading:
1174 1174 self._append_plain_text('\n')
1175 1175 self._reading = False
1176 1176 if self._reading_callback:
1177 1177 self._reading_callback()
1178 1178
1179 1179 # If the input buffer is a single line or there is only
1180 1180 # whitespace after the cursor, execute. Otherwise, split the
1181 1181 # line with a continuation prompt.
1182 1182 elif not self._executing:
1183 1183 cursor.movePosition(QtGui.QTextCursor.End,
1184 1184 QtGui.QTextCursor.KeepAnchor)
1185 1185 at_end = len(cursor.selectedText().strip()) == 0
1186 1186 single_line = (self._get_end_cursor().blockNumber() ==
1187 1187 self._get_prompt_cursor().blockNumber())
1188 1188 if (at_end or shift_down or single_line) and not ctrl_down:
1189 1189 self.execute(interactive = not shift_down)
1190 1190 else:
1191 1191 # Do this inside an edit block for clean undo/redo.
1192 1192 cursor.beginEditBlock()
1193 1193 cursor.setPosition(position)
1194 1194 cursor.insertText('\n')
1195 1195 self._insert_continuation_prompt(cursor)
1196 1196 cursor.endEditBlock()
1197 1197
1198 1198 # Ensure that the whole input buffer is visible.
1199 1199 # FIXME: This will not be usable if the input buffer is
1200 1200 # taller than the console widget.
1201 1201 self._control.moveCursor(QtGui.QTextCursor.End)
1202 1202 self._control.setTextCursor(cursor)
1203 1203
1204 1204 #------ Control/Cmd modifier -------------------------------------------
1205 1205
1206 1206 elif ctrl_down:
1207 1207 if key == QtCore.Qt.Key_G:
1208 1208 self._keyboard_quit()
1209 1209 intercepted = True
1210 1210
1211 1211 elif key == QtCore.Qt.Key_K:
1212 1212 if self._in_buffer(position):
1213 1213 cursor.clearSelection()
1214 1214 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1215 1215 QtGui.QTextCursor.KeepAnchor)
1216 1216 if not cursor.hasSelection():
1217 1217 # Line deletion (remove continuation prompt)
1218 1218 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1219 1219 QtGui.QTextCursor.KeepAnchor)
1220 1220 cursor.movePosition(QtGui.QTextCursor.Right,
1221 1221 QtGui.QTextCursor.KeepAnchor,
1222 1222 len(self._continuation_prompt))
1223 1223 self._kill_ring.kill_cursor(cursor)
1224 1224 self._set_cursor(cursor)
1225 1225 intercepted = True
1226 1226
1227 1227 elif key == QtCore.Qt.Key_L:
1228 1228 self.prompt_to_top()
1229 1229 intercepted = True
1230 1230
1231 1231 elif key == QtCore.Qt.Key_O:
1232 1232 if self._page_control and self._page_control.isVisible():
1233 1233 self._page_control.setFocus()
1234 1234 intercepted = True
1235 1235
1236 1236 elif key == QtCore.Qt.Key_U:
1237 1237 if self._in_buffer(position):
1238 1238 cursor.clearSelection()
1239 1239 start_line = cursor.blockNumber()
1240 1240 if start_line == self._get_prompt_cursor().blockNumber():
1241 1241 offset = len(self._prompt)
1242 1242 else:
1243 1243 offset = len(self._continuation_prompt)
1244 1244 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1245 1245 QtGui.QTextCursor.KeepAnchor)
1246 1246 cursor.movePosition(QtGui.QTextCursor.Right,
1247 1247 QtGui.QTextCursor.KeepAnchor, offset)
1248 1248 self._kill_ring.kill_cursor(cursor)
1249 1249 self._set_cursor(cursor)
1250 1250 intercepted = True
1251 1251
1252 1252 elif key == QtCore.Qt.Key_Y:
1253 1253 self._keep_cursor_in_buffer()
1254 1254 self._kill_ring.yank()
1255 1255 intercepted = True
1256 1256
1257 1257 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1258 1258 if key == QtCore.Qt.Key_Backspace:
1259 1259 cursor = self._get_word_start_cursor(position)
1260 1260 else: # key == QtCore.Qt.Key_Delete
1261 1261 cursor = self._get_word_end_cursor(position)
1262 1262 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1263 1263 self._kill_ring.kill_cursor(cursor)
1264 1264 intercepted = True
1265 1265
1266 1266 elif key == QtCore.Qt.Key_D:
1267 1267 if len(self.input_buffer) == 0:
1268 1268 self.exit_requested.emit(self)
1269 1269 else:
1270 1270 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1271 1271 QtCore.Qt.Key_Delete,
1272 1272 QtCore.Qt.NoModifier)
1273 1273 QtGui.qApp.sendEvent(self._control, new_event)
1274 1274 intercepted = True
1275 1275
1276 1276 #------ Alt modifier ---------------------------------------------------
1277 1277
1278 1278 elif alt_down:
1279 1279 if key == QtCore.Qt.Key_B:
1280 1280 self._set_cursor(self._get_word_start_cursor(position))
1281 1281 intercepted = True
1282 1282
1283 1283 elif key == QtCore.Qt.Key_F:
1284 1284 self._set_cursor(self._get_word_end_cursor(position))
1285 1285 intercepted = True
1286 1286
1287 1287 elif key == QtCore.Qt.Key_Y:
1288 1288 self._kill_ring.rotate()
1289 1289 intercepted = True
1290 1290
1291 1291 elif key == QtCore.Qt.Key_Backspace:
1292 1292 cursor = self._get_word_start_cursor(position)
1293 1293 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1294 1294 self._kill_ring.kill_cursor(cursor)
1295 1295 intercepted = True
1296 1296
1297 1297 elif key == QtCore.Qt.Key_D:
1298 1298 cursor = self._get_word_end_cursor(position)
1299 1299 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1300 1300 self._kill_ring.kill_cursor(cursor)
1301 1301 intercepted = True
1302 1302
1303 1303 elif key == QtCore.Qt.Key_Delete:
1304 1304 intercepted = True
1305 1305
1306 1306 elif key == QtCore.Qt.Key_Greater:
1307 1307 self._control.moveCursor(QtGui.QTextCursor.End)
1308 1308 intercepted = True
1309 1309
1310 1310 elif key == QtCore.Qt.Key_Less:
1311 1311 self._control.setTextCursor(self._get_prompt_cursor())
1312 1312 intercepted = True
1313 1313
1314 1314 #------ No modifiers ---------------------------------------------------
1315 1315
1316 1316 else:
1317 1317 if shift_down:
1318 1318 anchormode = QtGui.QTextCursor.KeepAnchor
1319 1319 else:
1320 1320 anchormode = QtGui.QTextCursor.MoveAnchor
1321 1321
1322 1322 if key == QtCore.Qt.Key_Escape:
1323 1323 self._keyboard_quit()
1324 1324 intercepted = True
1325 1325
1326 1326 elif key == QtCore.Qt.Key_Up:
1327 1327 if self._reading or not self._up_pressed(shift_down):
1328 1328 intercepted = True
1329 1329 else:
1330 1330 prompt_line = self._get_prompt_cursor().blockNumber()
1331 1331 intercepted = cursor.blockNumber() <= prompt_line
1332 1332
1333 1333 elif key == QtCore.Qt.Key_Down:
1334 1334 if self._reading or not self._down_pressed(shift_down):
1335 1335 intercepted = True
1336 1336 else:
1337 1337 end_line = self._get_end_cursor().blockNumber()
1338 1338 intercepted = cursor.blockNumber() == end_line
1339 1339
1340 1340 elif key == QtCore.Qt.Key_Tab:
1341 1341 if not self._reading:
1342 1342 if self._tab_pressed():
1343 1343 # real tab-key, insert four spaces
1344 1344 cursor.insertText(' '*4)
1345 1345 intercepted = True
1346 1346
1347 1347 elif key == QtCore.Qt.Key_Left:
1348 1348
1349 1349 # Move to the previous line
1350 1350 line, col = cursor.blockNumber(), cursor.columnNumber()
1351 1351 if line > self._get_prompt_cursor().blockNumber() and \
1352 1352 col == len(self._continuation_prompt):
1353 1353 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1354 1354 mode=anchormode)
1355 1355 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1356 1356 mode=anchormode)
1357 1357 intercepted = True
1358 1358
1359 1359 # Regular left movement
1360 1360 else:
1361 1361 intercepted = not self._in_buffer(position - 1)
1362 1362
1363 1363 elif key == QtCore.Qt.Key_Right:
1364 1364 original_block_number = cursor.blockNumber()
1365 1365 cursor.movePosition(QtGui.QTextCursor.Right,
1366 1366 mode=anchormode)
1367 1367 if cursor.blockNumber() != original_block_number:
1368 1368 cursor.movePosition(QtGui.QTextCursor.Right,
1369 1369 n=len(self._continuation_prompt),
1370 1370 mode=anchormode)
1371 1371 self._set_cursor(cursor)
1372 1372 intercepted = True
1373 1373
1374 1374 elif key == QtCore.Qt.Key_Home:
1375 1375 start_line = cursor.blockNumber()
1376 1376 if start_line == self._get_prompt_cursor().blockNumber():
1377 1377 start_pos = self._prompt_pos
1378 1378 else:
1379 1379 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1380 1380 QtGui.QTextCursor.KeepAnchor)
1381 1381 start_pos = cursor.position()
1382 1382 start_pos += len(self._continuation_prompt)
1383 1383 cursor.setPosition(position)
1384 1384 if shift_down and self._in_buffer(position):
1385 1385 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1386 1386 else:
1387 1387 cursor.setPosition(start_pos)
1388 1388 self._set_cursor(cursor)
1389 1389 intercepted = True
1390 1390
1391 1391 elif key == QtCore.Qt.Key_Backspace:
1392 1392
1393 1393 # Line deletion (remove continuation prompt)
1394 1394 line, col = cursor.blockNumber(), cursor.columnNumber()
1395 1395 if not self._reading and \
1396 1396 col == len(self._continuation_prompt) and \
1397 1397 line > self._get_prompt_cursor().blockNumber():
1398 1398 cursor.beginEditBlock()
1399 1399 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1400 1400 QtGui.QTextCursor.KeepAnchor)
1401 1401 cursor.removeSelectedText()
1402 1402 cursor.deletePreviousChar()
1403 1403 cursor.endEditBlock()
1404 1404 intercepted = True
1405 1405
1406 1406 # Regular backwards deletion
1407 1407 else:
1408 1408 anchor = cursor.anchor()
1409 1409 if anchor == position:
1410 1410 intercepted = not self._in_buffer(position - 1)
1411 1411 else:
1412 1412 intercepted = not self._in_buffer(min(anchor, position))
1413 1413
1414 1414 elif key == QtCore.Qt.Key_Delete:
1415 1415
1416 1416 # Line deletion (remove continuation prompt)
1417 1417 if not self._reading and self._in_buffer(position) and \
1418 1418 cursor.atBlockEnd() and not cursor.hasSelection():
1419 1419 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1420 1420 QtGui.QTextCursor.KeepAnchor)
1421 1421 cursor.movePosition(QtGui.QTextCursor.Right,
1422 1422 QtGui.QTextCursor.KeepAnchor,
1423 1423 len(self._continuation_prompt))
1424 1424 cursor.removeSelectedText()
1425 1425 intercepted = True
1426 1426
1427 1427 # Regular forwards deletion:
1428 1428 else:
1429 1429 anchor = cursor.anchor()
1430 1430 intercepted = (not self._in_buffer(anchor) or
1431 1431 not self._in_buffer(position))
1432 1432
1433 1433 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1434 1434 # using the keyboard in any part of the buffer. Also, permit scrolling
1435 1435 # with Page Up/Down keys. Finally, if we're executing, don't move the
1436 1436 # cursor (if even this made sense, we can't guarantee that the prompt
1437 1437 # position is still valid due to text truncation).
1438 1438 if not (self._control_key_down(event.modifiers(), include_command=True)
1439 1439 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1440 1440 or (self._executing and not self._reading)):
1441 1441 self._keep_cursor_in_buffer()
1442 1442
1443 1443 return intercepted
1444 1444
1445 1445 def _event_filter_page_keypress(self, event):
1446 1446 """ Filter key events for the paging widget to create console-like
1447 1447 interface.
1448 1448 """
1449 1449 key = event.key()
1450 1450 ctrl_down = self._control_key_down(event.modifiers())
1451 1451 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1452 1452
1453 1453 if ctrl_down:
1454 1454 if key == QtCore.Qt.Key_O:
1455 1455 self._control.setFocus()
1456 1456 intercept = True
1457 1457
1458 1458 elif alt_down:
1459 1459 if key == QtCore.Qt.Key_Greater:
1460 1460 self._page_control.moveCursor(QtGui.QTextCursor.End)
1461 1461 intercepted = True
1462 1462
1463 1463 elif key == QtCore.Qt.Key_Less:
1464 1464 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1465 1465 intercepted = True
1466 1466
1467 1467 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1468 1468 if self._splitter:
1469 1469 self._page_control.hide()
1470 1470 self._control.setFocus()
1471 1471 else:
1472 1472 self.layout().setCurrentWidget(self._control)
1473 1473 return True
1474 1474
1475 1475 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1476 1476 QtCore.Qt.Key_Tab):
1477 1477 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1478 1478 QtCore.Qt.Key_PageDown,
1479 1479 QtCore.Qt.NoModifier)
1480 1480 QtGui.qApp.sendEvent(self._page_control, new_event)
1481 1481 return True
1482 1482
1483 1483 elif key == QtCore.Qt.Key_Backspace:
1484 1484 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1485 1485 QtCore.Qt.Key_PageUp,
1486 1486 QtCore.Qt.NoModifier)
1487 1487 QtGui.qApp.sendEvent(self._page_control, new_event)
1488 1488 return True
1489 1489
1490 1490 return False
1491 1491
1492 1492 def _flush_pending_stream(self):
1493 1493 """ Flush out pending text into the widget. """
1494 1494 text = self._pending_insert_text
1495 1495 self._pending_insert_text = []
1496 1496 buffer_size = self._control.document().maximumBlockCount()
1497 1497 if buffer_size > 0:
1498 1498 text = self._get_last_lines_from_list(text, buffer_size)
1499 1499 text = ''.join(text)
1500 1500 t = time.time()
1501 1501 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1502 1502 # Set the flush interval to equal the maximum time to update text.
1503 1503 self._pending_text_flush_interval.setInterval(max(100,
1504 1504 (time.time()-t)*1000))
1505 1505
1506 1506 def _format_as_columns(self, items, separator=' '):
1507 1507 """ Transform a list of strings into a single string with columns.
1508 1508
1509 1509 Parameters
1510 1510 ----------
1511 1511 items : sequence of strings
1512 1512 The strings to process.
1513 1513
1514 1514 separator : str, optional [default is two spaces]
1515 1515 The string that separates columns.
1516 1516
1517 1517 Returns
1518 1518 -------
1519 1519 The formatted string.
1520 1520 """
1521 1521 # Calculate the number of characters available.
1522 1522 width = self._control.viewport().width()
1523 1523 char_width = QtGui.QFontMetrics(self.font).width(' ')
1524 1524 displaywidth = max(10, (width / char_width) - 1)
1525 1525
1526 1526 return columnize(items, separator, displaywidth)
1527 1527
1528 1528 def _get_block_plain_text(self, block):
1529 1529 """ Given a QTextBlock, return its unformatted text.
1530 1530 """
1531 1531 cursor = QtGui.QTextCursor(block)
1532 1532 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1533 1533 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1534 1534 QtGui.QTextCursor.KeepAnchor)
1535 1535 return cursor.selection().toPlainText()
1536 1536
1537 1537 def _get_cursor(self):
1538 1538 """ Convenience method that returns a cursor for the current position.
1539 1539 """
1540 1540 return self._control.textCursor()
1541 1541
1542 1542 def _get_end_cursor(self):
1543 1543 """ Convenience method that returns a cursor for the last character.
1544 1544 """
1545 1545 cursor = self._control.textCursor()
1546 1546 cursor.movePosition(QtGui.QTextCursor.End)
1547 1547 return cursor
1548 1548
1549 1549 def _get_input_buffer_cursor_column(self):
1550 1550 """ Returns the column of the cursor in the input buffer, excluding the
1551 1551 contribution by the prompt, or -1 if there is no such column.
1552 1552 """
1553 1553 prompt = self._get_input_buffer_cursor_prompt()
1554 1554 if prompt is None:
1555 1555 return -1
1556 1556 else:
1557 1557 cursor = self._control.textCursor()
1558 1558 return cursor.columnNumber() - len(prompt)
1559 1559
1560 1560 def _get_input_buffer_cursor_line(self):
1561 1561 """ Returns the text of the line of the input buffer that contains the
1562 1562 cursor, or None if there is no such line.
1563 1563 """
1564 1564 prompt = self._get_input_buffer_cursor_prompt()
1565 1565 if prompt is None:
1566 1566 return None
1567 1567 else:
1568 1568 cursor = self._control.textCursor()
1569 1569 text = self._get_block_plain_text(cursor.block())
1570 1570 return text[len(prompt):]
1571 1571
1572 1572 def _get_input_buffer_cursor_prompt(self):
1573 1573 """ Returns the (plain text) prompt for line of the input buffer that
1574 1574 contains the cursor, or None if there is no such line.
1575 1575 """
1576 1576 if self._executing:
1577 1577 return None
1578 1578 cursor = self._control.textCursor()
1579 1579 if cursor.position() >= self._prompt_pos:
1580 1580 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1581 1581 return self._prompt
1582 1582 else:
1583 1583 return self._continuation_prompt
1584 1584 else:
1585 1585 return None
1586 1586
1587 1587 def _get_last_lines(self, text, num_lines, return_count=False):
1588 1588 """ Return last specified number of lines of text (like `tail -n`).
1589 1589 If return_count is True, returns a tuple of clipped text and the
1590 1590 number of lines in the clipped text.
1591 1591 """
1592 1592 pos = len(text)
1593 1593 if pos < num_lines:
1594 1594 if return_count:
1595 1595 return text, text.count('\n') if return_count else text
1596 1596 else:
1597 1597 return text
1598 1598 i = 0
1599 1599 while i < num_lines:
1600 1600 pos = text.rfind('\n', None, pos)
1601 1601 if pos == -1:
1602 1602 pos = None
1603 1603 break
1604 1604 i += 1
1605 1605 if return_count:
1606 1606 return text[pos:], i
1607 1607 else:
1608 1608 return text[pos:]
1609 1609
1610 1610 def _get_last_lines_from_list(self, text_list, num_lines):
1611 1611 """ Return the list of text clipped to last specified lines.
1612 1612 """
1613 1613 ret = []
1614 1614 lines_pending = num_lines
1615 1615 for text in reversed(text_list):
1616 1616 text, lines_added = self._get_last_lines(text, lines_pending,
1617 1617 return_count=True)
1618 1618 ret.append(text)
1619 1619 lines_pending -= lines_added
1620 1620 if lines_pending <= 0:
1621 1621 break
1622 1622 return ret[::-1]
1623 1623
1624 1624 def _get_prompt_cursor(self):
1625 1625 """ Convenience method that returns a cursor for the prompt position.
1626 1626 """
1627 1627 cursor = self._control.textCursor()
1628 1628 cursor.setPosition(self._prompt_pos)
1629 1629 return cursor
1630 1630
1631 1631 def _get_selection_cursor(self, start, end):
1632 1632 """ Convenience method that returns a cursor with text selected between
1633 1633 the positions 'start' and 'end'.
1634 1634 """
1635 1635 cursor = self._control.textCursor()
1636 1636 cursor.setPosition(start)
1637 1637 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1638 1638 return cursor
1639 1639
1640 1640 def _get_word_start_cursor(self, position):
1641 1641 """ Find the start of the word to the left the given position. If a
1642 1642 sequence of non-word characters precedes the first word, skip over
1643 1643 them. (This emulates the behavior of bash, emacs, etc.)
1644 1644 """
1645 1645 document = self._control.document()
1646 1646 position -= 1
1647 1647 while position >= self._prompt_pos and \
1648 1648 not is_letter_or_number(document.characterAt(position)):
1649 1649 position -= 1
1650 1650 while position >= self._prompt_pos and \
1651 1651 is_letter_or_number(document.characterAt(position)):
1652 1652 position -= 1
1653 1653 cursor = self._control.textCursor()
1654 1654 cursor.setPosition(position + 1)
1655 1655 return cursor
1656 1656
1657 1657 def _get_word_end_cursor(self, position):
1658 1658 """ Find the end of the word to the right the given position. If a
1659 1659 sequence of non-word characters precedes the first word, skip over
1660 1660 them. (This emulates the behavior of bash, emacs, etc.)
1661 1661 """
1662 1662 document = self._control.document()
1663 1663 end = self._get_end_cursor().position()
1664 1664 while position < end and \
1665 1665 not is_letter_or_number(document.characterAt(position)):
1666 1666 position += 1
1667 1667 while position < end and \
1668 1668 is_letter_or_number(document.characterAt(position)):
1669 1669 position += 1
1670 1670 cursor = self._control.textCursor()
1671 1671 cursor.setPosition(position)
1672 1672 return cursor
1673 1673
1674 1674 def _insert_continuation_prompt(self, cursor):
1675 1675 """ Inserts new continuation prompt using the specified cursor.
1676 1676 """
1677 1677 if self._continuation_prompt_html is None:
1678 1678 self._insert_plain_text(cursor, self._continuation_prompt)
1679 1679 else:
1680 1680 self._continuation_prompt = self._insert_html_fetching_plain_text(
1681 1681 cursor, self._continuation_prompt_html)
1682 1682
1683 1683 def _insert_block(self, cursor, block_format=None):
1684 1684 """ Inserts an empty QTextBlock using the specified cursor.
1685 1685 """
1686 1686 if block_format is None:
1687 1687 block_format = QtGui.QTextBlockFormat()
1688 1688 cursor.insertBlock(block_format)
1689 1689
1690 1690 def _insert_html(self, cursor, html):
1691 1691 """ Inserts HTML using the specified cursor in such a way that future
1692 1692 formatting is unaffected.
1693 1693 """
1694 1694 cursor.beginEditBlock()
1695 1695 cursor.insertHtml(html)
1696 1696
1697 1697 # After inserting HTML, the text document "remembers" it's in "html
1698 1698 # mode", which means that subsequent calls adding plain text will result
1699 1699 # in unwanted formatting, lost tab characters, etc. The following code
1700 1700 # hacks around this behavior, which I consider to be a bug in Qt, by
1701 1701 # (crudely) resetting the document's style state.
1702 1702 cursor.movePosition(QtGui.QTextCursor.Left,
1703 1703 QtGui.QTextCursor.KeepAnchor)
1704 1704 if cursor.selection().toPlainText() == ' ':
1705 1705 cursor.removeSelectedText()
1706 1706 else:
1707 1707 cursor.movePosition(QtGui.QTextCursor.Right)
1708 1708 cursor.insertText(' ', QtGui.QTextCharFormat())
1709 1709 cursor.endEditBlock()
1710 1710
1711 1711 def _insert_html_fetching_plain_text(self, cursor, html):
1712 1712 """ Inserts HTML using the specified cursor, then returns its plain text
1713 1713 version.
1714 1714 """
1715 1715 cursor.beginEditBlock()
1716 1716 cursor.removeSelectedText()
1717 1717
1718 1718 start = cursor.position()
1719 1719 self._insert_html(cursor, html)
1720 1720 end = cursor.position()
1721 1721 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1722 1722 text = cursor.selection().toPlainText()
1723 1723
1724 1724 cursor.setPosition(end)
1725 1725 cursor.endEditBlock()
1726 1726 return text
1727 1727
1728 1728 def _insert_plain_text(self, cursor, text, flush=False):
1729 1729 """ Inserts plain text using the specified cursor, processing ANSI codes
1730 1730 if enabled.
1731 1731 """
1732 1732 # maximumBlockCount() can be different from self.buffer_size in
1733 1733 # case input prompt is active.
1734 1734 buffer_size = self._control.document().maximumBlockCount()
1735 1735
1736 1736 if self._executing and not flush and \
1737 1737 self._pending_text_flush_interval.isActive():
1738 1738 self._pending_insert_text.append(text)
1739 1739 if buffer_size > 0:
1740 1740 self._pending_insert_text = self._get_last_lines_from_list(
1741 1741 self._pending_insert_text, buffer_size)
1742 1742 return
1743 1743
1744 1744 if self._executing and not self._pending_text_flush_interval.isActive():
1745 1745 self._pending_text_flush_interval.start()
1746 1746
1747 1747 # Clip the text to last `buffer_size` lines.
1748 1748 if buffer_size > 0:
1749 1749 text = self._get_last_lines(text, buffer_size)
1750 1750
1751 1751 cursor.beginEditBlock()
1752 1752 if self.ansi_codes:
1753 1753 for substring in self._ansi_processor.split_string(text):
1754 1754 for act in self._ansi_processor.actions:
1755 1755
1756 1756 # Unlike real terminal emulators, we don't distinguish
1757 1757 # between the screen and the scrollback buffer. A screen
1758 1758 # erase request clears everything.
1759 1759 if act.action == 'erase' and act.area == 'screen':
1760 1760 cursor.select(QtGui.QTextCursor.Document)
1761 1761 cursor.removeSelectedText()
1762 1762
1763 1763 # Simulate a form feed by scrolling just past the last line.
1764 1764 elif act.action == 'scroll' and act.unit == 'page':
1765 1765 cursor.insertText('\n')
1766 1766 cursor.endEditBlock()
1767 1767 self._set_top_cursor(cursor)
1768 1768 cursor.joinPreviousEditBlock()
1769 1769 cursor.deletePreviousChar()
1770 1770
1771 1771 elif act.action == 'carriage-return':
1772 1772 cursor.movePosition(
1773 1773 cursor.StartOfLine, cursor.KeepAnchor)
1774 1774
1775 1775 elif act.action == 'beep':
1776 1776 QtGui.qApp.beep()
1777 1777
1778 1778 elif act.action == 'backspace':
1779 1779 if not cursor.atBlockStart():
1780 1780 cursor.movePosition(
1781 1781 cursor.PreviousCharacter, cursor.KeepAnchor)
1782 1782
1783 1783 elif act.action == 'newline':
1784 1784 cursor.movePosition(cursor.EndOfLine)
1785 1785
1786 1786 format = self._ansi_processor.get_format()
1787 1787
1788 1788 selection = cursor.selectedText()
1789 1789 if len(selection) == 0:
1790 1790 cursor.insertText(substring, format)
1791 1791 elif substring is not None:
1792 1792 # BS and CR are treated as a change in print
1793 1793 # position, rather than a backwards character
1794 1794 # deletion for output equivalence with (I)Python
1795 1795 # terminal.
1796 1796 if len(substring) >= len(selection):
1797 1797 cursor.insertText(substring, format)
1798 1798 else:
1799 1799 old_text = selection[len(substring):]
1800 1800 cursor.insertText(substring + old_text, format)
1801 1801 cursor.movePosition(cursor.PreviousCharacter,
1802 1802 cursor.KeepAnchor, len(old_text))
1803 1803 else:
1804 1804 cursor.insertText(text)
1805 1805 cursor.endEditBlock()
1806 1806
1807 1807 def _insert_plain_text_into_buffer(self, cursor, text):
1808 1808 """ Inserts text into the input buffer using the specified cursor (which
1809 1809 must be in the input buffer), ensuring that continuation prompts are
1810 1810 inserted as necessary.
1811 1811 """
1812 1812 lines = text.splitlines(True)
1813 1813 if lines:
1814 1814 cursor.beginEditBlock()
1815 1815 cursor.insertText(lines[0])
1816 1816 for line in lines[1:]:
1817 1817 if self._continuation_prompt_html is None:
1818 1818 cursor.insertText(self._continuation_prompt)
1819 1819 else:
1820 1820 self._continuation_prompt = \
1821 1821 self._insert_html_fetching_plain_text(
1822 1822 cursor, self._continuation_prompt_html)
1823 1823 cursor.insertText(line)
1824 1824 cursor.endEditBlock()
1825 1825
1826 1826 def _in_buffer(self, position=None):
1827 1827 """ Returns whether the current cursor (or, if specified, a position) is
1828 1828 inside the editing region.
1829 1829 """
1830 1830 cursor = self._control.textCursor()
1831 1831 if position is None:
1832 1832 position = cursor.position()
1833 1833 else:
1834 1834 cursor.setPosition(position)
1835 1835 line = cursor.blockNumber()
1836 1836 prompt_line = self._get_prompt_cursor().blockNumber()
1837 1837 if line == prompt_line:
1838 1838 return position >= self._prompt_pos
1839 1839 elif line > prompt_line:
1840 1840 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1841 1841 prompt_pos = cursor.position() + len(self._continuation_prompt)
1842 1842 return position >= prompt_pos
1843 1843 return False
1844 1844
1845 1845 def _keep_cursor_in_buffer(self):
1846 1846 """ Ensures that the cursor is inside the editing region. Returns
1847 1847 whether the cursor was moved.
1848 1848 """
1849 1849 moved = not self._in_buffer()
1850 1850 if moved:
1851 1851 cursor = self._control.textCursor()
1852 1852 cursor.movePosition(QtGui.QTextCursor.End)
1853 1853 self._control.setTextCursor(cursor)
1854 1854 return moved
1855 1855
1856 1856 def _keyboard_quit(self):
1857 1857 """ Cancels the current editing task ala Ctrl-G in Emacs.
1858 1858 """
1859 1859 if self._temp_buffer_filled :
1860 1860 self._cancel_completion()
1861 1861 self._clear_temporary_buffer()
1862 1862 else:
1863 1863 self.input_buffer = ''
1864 1864
1865 1865 def _page(self, text, html=False):
1866 1866 """ Displays text using the pager if it exceeds the height of the
1867 1867 viewport.
1868 1868
1869 1869 Parameters:
1870 1870 -----------
1871 1871 html : bool, optional (default False)
1872 1872 If set, the text will be interpreted as HTML instead of plain text.
1873 1873 """
1874 1874 line_height = QtGui.QFontMetrics(self.font).height()
1875 1875 minlines = self._control.viewport().height() / line_height
1876 1876 if self.paging != 'none' and \
1877 1877 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1878 1878 if self.paging == 'custom':
1879 1879 self.custom_page_requested.emit(text)
1880 1880 else:
1881 1881 self._page_control.clear()
1882 1882 cursor = self._page_control.textCursor()
1883 1883 if html:
1884 1884 self._insert_html(cursor, text)
1885 1885 else:
1886 1886 self._insert_plain_text(cursor, text)
1887 1887 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1888 1888
1889 1889 self._page_control.viewport().resize(self._control.size())
1890 1890 if self._splitter:
1891 1891 self._page_control.show()
1892 1892 self._page_control.setFocus()
1893 1893 else:
1894 1894 self.layout().setCurrentWidget(self._page_control)
1895 1895 elif html:
1896 1896 self._append_html(text)
1897 1897 else:
1898 1898 self._append_plain_text(text)
1899 1899
1900 1900 def _set_paging(self, paging):
1901 1901 """
1902 1902 Change the pager to `paging` style.
1903 1903
1904 1904 XXX: currently, this is limited to switching between 'hsplit' and
1905 1905 'vsplit'.
1906 1906
1907 1907 Parameters:
1908 1908 -----------
1909 1909 paging : string
1910 1910 Either "hsplit", "vsplit", or "inside"
1911 1911 """
1912 1912 if self._splitter is None:
1913 1913 raise NotImplementedError("""can only switch if --paging=hsplit or
1914 1914 --paging=vsplit is used.""")
1915 1915 if paging == 'hsplit':
1916 1916 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1917 1917 elif paging == 'vsplit':
1918 1918 self._splitter.setOrientation(QtCore.Qt.Vertical)
1919 1919 elif paging == 'inside':
1920 1920 raise NotImplementedError("""switching to 'inside' paging not
1921 1921 supported yet.""")
1922 1922 else:
1923 1923 raise ValueError("unknown paging method '%s'" % paging)
1924 1924 self.paging = paging
1925 1925
1926 1926 def _prompt_finished(self):
1927 1927 """ Called immediately after a prompt is finished, i.e. when some input
1928 1928 will be processed and a new prompt displayed.
1929 1929 """
1930 1930 self._control.setReadOnly(True)
1931 1931 self._prompt_finished_hook()
1932 1932
1933 1933 def _prompt_started(self):
1934 1934 """ Called immediately after a new prompt is displayed.
1935 1935 """
1936 1936 # Temporarily disable the maximum block count to permit undo/redo and
1937 1937 # to ensure that the prompt position does not change due to truncation.
1938 1938 self._control.document().setMaximumBlockCount(0)
1939 1939 self._control.setUndoRedoEnabled(True)
1940 1940
1941 1941 # Work around bug in QPlainTextEdit: input method is not re-enabled
1942 1942 # when read-only is disabled.
1943 1943 self._control.setReadOnly(False)
1944 1944 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1945 1945
1946 1946 if not self._reading:
1947 1947 self._executing = False
1948 1948 self._prompt_started_hook()
1949 1949
1950 1950 # If the input buffer has changed while executing, load it.
1951 1951 if self._input_buffer_pending:
1952 1952 self.input_buffer = self._input_buffer_pending
1953 1953 self._input_buffer_pending = ''
1954 1954
1955 1955 self._control.moveCursor(QtGui.QTextCursor.End)
1956 1956
1957 1957 def _readline(self, prompt='', callback=None):
1958 1958 """ Reads one line of input from the user.
1959 1959
1960 1960 Parameters
1961 1961 ----------
1962 1962 prompt : str, optional
1963 1963 The prompt to print before reading the line.
1964 1964
1965 1965 callback : callable, optional
1966 1966 A callback to execute with the read line. If not specified, input is
1967 1967 read *synchronously* and this method does not return until it has
1968 1968 been read.
1969 1969
1970 1970 Returns
1971 1971 -------
1972 1972 If a callback is specified, returns nothing. Otherwise, returns the
1973 1973 input string with the trailing newline stripped.
1974 1974 """
1975 1975 if self._reading:
1976 1976 raise RuntimeError('Cannot read a line. Widget is already reading.')
1977 1977
1978 1978 if not callback and not self.isVisible():
1979 1979 # If the user cannot see the widget, this function cannot return.
1980 1980 raise RuntimeError('Cannot synchronously read a line if the widget '
1981 1981 'is not visible!')
1982 1982
1983 1983 self._reading = True
1984 1984 self._show_prompt(prompt, newline=False)
1985 1985
1986 1986 if callback is None:
1987 1987 self._reading_callback = None
1988 1988 while self._reading:
1989 1989 QtCore.QCoreApplication.processEvents()
1990 1990 return self._get_input_buffer(force=True).rstrip('\n')
1991 1991
1992 1992 else:
1993 1993 self._reading_callback = lambda: \
1994 1994 callback(self._get_input_buffer(force=True).rstrip('\n'))
1995 1995
1996 1996 def _set_continuation_prompt(self, prompt, html=False):
1997 1997 """ Sets the continuation prompt.
1998 1998
1999 1999 Parameters
2000 2000 ----------
2001 2001 prompt : str
2002 2002 The prompt to show when more input is needed.
2003 2003
2004 2004 html : bool, optional (default False)
2005 2005 If set, the prompt will be inserted as formatted HTML. Otherwise,
2006 2006 the prompt will be treated as plain text, though ANSI color codes
2007 2007 will be handled.
2008 2008 """
2009 2009 if html:
2010 2010 self._continuation_prompt_html = prompt
2011 2011 else:
2012 2012 self._continuation_prompt = prompt
2013 2013 self._continuation_prompt_html = None
2014 2014
2015 2015 def _set_cursor(self, cursor):
2016 2016 """ Convenience method to set the current cursor.
2017 2017 """
2018 2018 self._control.setTextCursor(cursor)
2019 2019
2020 2020 def _set_top_cursor(self, cursor):
2021 2021 """ Scrolls the viewport so that the specified cursor is at the top.
2022 2022 """
2023 2023 scrollbar = self._control.verticalScrollBar()
2024 2024 scrollbar.setValue(scrollbar.maximum())
2025 2025 original_cursor = self._control.textCursor()
2026 2026 self._control.setTextCursor(cursor)
2027 2027 self._control.ensureCursorVisible()
2028 2028 self._control.setTextCursor(original_cursor)
2029 2029
2030 2030 def _show_prompt(self, prompt=None, html=False, newline=True):
2031 2031 """ Writes a new prompt at the end of the buffer.
2032 2032
2033 2033 Parameters
2034 2034 ----------
2035 2035 prompt : str, optional
2036 2036 The prompt to show. If not specified, the previous prompt is used.
2037 2037
2038 2038 html : bool, optional (default False)
2039 2039 Only relevant when a prompt is specified. If set, the prompt will
2040 2040 be inserted as formatted HTML. Otherwise, the prompt will be treated
2041 2041 as plain text, though ANSI color codes will be handled.
2042 2042
2043 2043 newline : bool, optional (default True)
2044 2044 If set, a new line will be written before showing the prompt if
2045 2045 there is not already a newline at the end of the buffer.
2046 2046 """
2047 2047 # Save the current end position to support _append*(before_prompt=True).
2048 2048 cursor = self._get_end_cursor()
2049 2049 self._append_before_prompt_pos = cursor.position()
2050 2050
2051 2051 # Insert a preliminary newline, if necessary.
2052 2052 if newline and cursor.position() > 0:
2053 2053 cursor.movePosition(QtGui.QTextCursor.Left,
2054 2054 QtGui.QTextCursor.KeepAnchor)
2055 2055 if cursor.selection().toPlainText() != '\n':
2056 2056 self._append_block()
2057 2057
2058 2058 # Write the prompt.
2059 2059 self._append_plain_text(self._prompt_sep)
2060 2060 if prompt is None:
2061 2061 if self._prompt_html is None:
2062 2062 self._append_plain_text(self._prompt)
2063 2063 else:
2064 2064 self._append_html(self._prompt_html)
2065 2065 else:
2066 2066 if html:
2067 2067 self._prompt = self._append_html_fetching_plain_text(prompt)
2068 2068 self._prompt_html = prompt
2069 2069 else:
2070 2070 self._append_plain_text(prompt)
2071 2071 self._prompt = prompt
2072 2072 self._prompt_html = None
2073 2073
2074 2074 self._flush_pending_stream()
2075 2075 self._prompt_pos = self._get_end_cursor().position()
2076 2076 self._prompt_started()
2077 2077
2078 2078 #------ Signal handlers ----------------------------------------------------
2079 2079
2080 2080 def _adjust_scrollbars(self):
2081 2081 """ Expands the vertical scrollbar beyond the range set by Qt.
2082 2082 """
2083 2083 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2084 2084 # and qtextedit.cpp.
2085 2085 document = self._control.document()
2086 2086 scrollbar = self._control.verticalScrollBar()
2087 2087 viewport_height = self._control.viewport().height()
2088 2088 if isinstance(self._control, QtGui.QPlainTextEdit):
2089 2089 maximum = max(0, document.lineCount() - 1)
2090 2090 step = viewport_height / self._control.fontMetrics().lineSpacing()
2091 2091 else:
2092 2092 # QTextEdit does not do line-based layout and blocks will not in
2093 2093 # general have the same height. Therefore it does not make sense to
2094 2094 # attempt to scroll in line height increments.
2095 2095 maximum = document.size().height()
2096 2096 step = viewport_height
2097 2097 diff = maximum - scrollbar.maximum()
2098 2098 scrollbar.setRange(0, maximum)
2099 2099 scrollbar.setPageStep(step)
2100 2100
2101 2101 # Compensate for undesirable scrolling that occurs automatically due to
2102 2102 # maximumBlockCount() text truncation.
2103 2103 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2104 2104 scrollbar.setValue(scrollbar.value() + diff)
2105 2105
2106 2106 def _custom_context_menu_requested(self, pos):
2107 2107 """ Shows a context menu at the given QPoint (in widget coordinates).
2108 2108 """
2109 2109 menu = self._context_menu_make(pos)
2110 2110 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,784 +1,784 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import uuid
7 7
8 8 # System library imports
9 9 from pygments.lexers import PythonLexer
10 10 from IPython.external import qt
11 11 from IPython.external.qt import QtCore, QtGui
12 12
13 13 # Local imports
14 14 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
15 15 from IPython.core.inputtransformer import classic_prompt
16 16 from IPython.core.oinspect import call_tip
17 17 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
18 18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 from bracket_matcher import BracketMatcher
20 from call_tip_widget import CallTipWidget
21 from completion_lexer import CompletionLexer
22 from history_console_widget import HistoryConsoleWidget
23 from pygments_highlighter import PygmentsHighlighter
19 from .bracket_matcher import BracketMatcher
20 from .call_tip_widget import CallTipWidget
21 from .completion_lexer import CompletionLexer
22 from .history_console_widget import HistoryConsoleWidget
23 from .pygments_highlighter import PygmentsHighlighter
24 24
25 25
26 26 class FrontendHighlighter(PygmentsHighlighter):
27 27 """ A PygmentsHighlighter that understands and ignores prompts.
28 28 """
29 29
30 30 def __init__(self, frontend):
31 31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 32 self._current_offset = 0
33 33 self._frontend = frontend
34 34 self.highlighting_on = False
35 35
36 36 def highlightBlock(self, string):
37 37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 38 """
39 39 if not self.highlighting_on:
40 40 return
41 41
42 42 # The input to this function is a unicode string that may contain
43 43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 44 # the string as plain text so we can compare it.
45 45 current_block = self.currentBlock()
46 46 string = self._frontend._get_block_plain_text(current_block)
47 47
48 48 # Decide whether to check for the regular or continuation prompt.
49 49 if current_block.contains(self._frontend._prompt_pos):
50 50 prompt = self._frontend._prompt
51 51 else:
52 52 prompt = self._frontend._continuation_prompt
53 53
54 54 # Only highlight if we can identify a prompt, but make sure not to
55 55 # highlight the prompt.
56 56 if string.startswith(prompt):
57 57 self._current_offset = len(prompt)
58 58 string = string[len(prompt):]
59 59 super(FrontendHighlighter, self).highlightBlock(string)
60 60
61 61 def rehighlightBlock(self, block):
62 62 """ Reimplemented to temporarily enable highlighting if disabled.
63 63 """
64 64 old = self.highlighting_on
65 65 self.highlighting_on = True
66 66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 67 self.highlighting_on = old
68 68
69 69 def setFormat(self, start, count, format):
70 70 """ Reimplemented to highlight selectively.
71 71 """
72 72 start += self._current_offset
73 73 super(FrontendHighlighter, self).setFormat(start, count, format)
74 74
75 75
76 76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 77 """ A Qt frontend for a generic Python kernel.
78 78 """
79 79
80 80 # The text to show when the kernel is (re)started.
81 81 banner = Unicode(config=True)
82 82
83 83 # An option and corresponding signal for overriding the default kernel
84 84 # interrupt behavior.
85 85 custom_interrupt = Bool(False)
86 86 custom_interrupt_requested = QtCore.Signal()
87 87
88 88 # An option and corresponding signals for overriding the default kernel
89 89 # restart behavior.
90 90 custom_restart = Bool(False)
91 91 custom_restart_kernel_died = QtCore.Signal(float)
92 92 custom_restart_requested = QtCore.Signal()
93 93
94 94 # Whether to automatically show calltips on open-parentheses.
95 95 enable_calltips = Bool(True, config=True,
96 96 help="Whether to draw information calltips on open-parentheses.")
97 97
98 98 clear_on_kernel_restart = Bool(True, config=True,
99 99 help="Whether to clear the console when the kernel is restarted")
100 100
101 101 confirm_restart = Bool(True, config=True,
102 102 help="Whether to ask for user confirmation when restarting kernel")
103 103
104 104 # Emitted when a user visible 'execute_request' has been submitted to the
105 105 # kernel from the FrontendWidget. Contains the code to be executed.
106 106 executing = QtCore.Signal(object)
107 107
108 108 # Emitted when a user-visible 'execute_reply' has been received from the
109 109 # kernel and processed by the FrontendWidget. Contains the response message.
110 110 executed = QtCore.Signal(object)
111 111
112 112 # Emitted when an exit request has been received from the kernel.
113 113 exit_requested = QtCore.Signal(object)
114 114
115 115 # Protected class variables.
116 116 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
117 117 logical_line_transforms=[],
118 118 python_line_transforms=[],
119 119 )
120 120 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
121 121 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
122 122 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
123 123 _input_splitter_class = InputSplitter
124 124 _local_kernel = False
125 125 _highlighter = Instance(FrontendHighlighter)
126 126
127 127 #---------------------------------------------------------------------------
128 128 # 'object' interface
129 129 #---------------------------------------------------------------------------
130 130
131 131 def __init__(self, *args, **kw):
132 132 super(FrontendWidget, self).__init__(*args, **kw)
133 133 # FIXME: remove this when PySide min version is updated past 1.0.7
134 134 # forcefully disable calltips if PySide is < 1.0.7, because they crash
135 135 if qt.QT_API == qt.QT_API_PYSIDE:
136 136 import PySide
137 137 if PySide.__version_info__ < (1,0,7):
138 138 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
139 139 self.enable_calltips = False
140 140
141 141 # FrontendWidget protected variables.
142 142 self._bracket_matcher = BracketMatcher(self._control)
143 143 self._call_tip_widget = CallTipWidget(self._control)
144 144 self._completion_lexer = CompletionLexer(PythonLexer())
145 145 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
146 146 self._hidden = False
147 147 self._highlighter = FrontendHighlighter(self)
148 148 self._input_splitter = self._input_splitter_class()
149 149 self._kernel_manager = None
150 150 self._kernel_client = None
151 151 self._request_info = {}
152 152 self._request_info['execute'] = {};
153 153 self._callback_dict = {}
154 154
155 155 # Configure the ConsoleWidget.
156 156 self.tab_width = 4
157 157 self._set_continuation_prompt('... ')
158 158
159 159 # Configure the CallTipWidget.
160 160 self._call_tip_widget.setFont(self.font)
161 161 self.font_changed.connect(self._call_tip_widget.setFont)
162 162
163 163 # Configure actions.
164 164 action = self._copy_raw_action
165 165 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
166 166 action.setEnabled(False)
167 167 action.setShortcut(QtGui.QKeySequence(key))
168 168 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
169 169 action.triggered.connect(self.copy_raw)
170 170 self.copy_available.connect(action.setEnabled)
171 171 self.addAction(action)
172 172
173 173 # Connect signal handlers.
174 174 document = self._control.document()
175 175 document.contentsChange.connect(self._document_contents_change)
176 176
177 177 # Set flag for whether we are connected via localhost.
178 178 self._local_kernel = kw.get('local_kernel',
179 179 FrontendWidget._local_kernel)
180 180
181 181 #---------------------------------------------------------------------------
182 182 # 'ConsoleWidget' public interface
183 183 #---------------------------------------------------------------------------
184 184
185 185 def copy(self):
186 186 """ Copy the currently selected text to the clipboard, removing prompts.
187 187 """
188 188 if self._page_control is not None and self._page_control.hasFocus():
189 189 self._page_control.copy()
190 190 elif self._control.hasFocus():
191 191 text = self._control.textCursor().selection().toPlainText()
192 192 if text:
193 193 text = self._prompt_transformer.transform_cell(text)
194 194 QtGui.QApplication.clipboard().setText(text)
195 195 else:
196 196 self.log.debug("frontend widget : unknown copy target")
197 197
198 198 #---------------------------------------------------------------------------
199 199 # 'ConsoleWidget' abstract interface
200 200 #---------------------------------------------------------------------------
201 201
202 202 def _is_complete(self, source, interactive):
203 203 """ Returns whether 'source' can be completely processed and a new
204 204 prompt created. When triggered by an Enter/Return key press,
205 205 'interactive' is True; otherwise, it is False.
206 206 """
207 207 self._input_splitter.reset()
208 208 complete = self._input_splitter.push(source)
209 209 if interactive:
210 210 complete = not self._input_splitter.push_accepts_more()
211 211 return complete
212 212
213 213 def _execute(self, source, hidden):
214 214 """ Execute 'source'. If 'hidden', do not show any output.
215 215
216 216 See parent class :meth:`execute` docstring for full details.
217 217 """
218 218 msg_id = self.kernel_client.execute(source, hidden)
219 219 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
220 220 self._hidden = hidden
221 221 if not hidden:
222 222 self.executing.emit(source)
223 223
224 224 def _prompt_started_hook(self):
225 225 """ Called immediately after a new prompt is displayed.
226 226 """
227 227 if not self._reading:
228 228 self._highlighter.highlighting_on = True
229 229
230 230 def _prompt_finished_hook(self):
231 231 """ Called immediately after a prompt is finished, i.e. when some input
232 232 will be processed and a new prompt displayed.
233 233 """
234 234 # Flush all state from the input splitter so the next round of
235 235 # reading input starts with a clean buffer.
236 236 self._input_splitter.reset()
237 237
238 238 if not self._reading:
239 239 self._highlighter.highlighting_on = False
240 240
241 241 def _tab_pressed(self):
242 242 """ Called when the tab key is pressed. Returns whether to continue
243 243 processing the event.
244 244 """
245 245 # Perform tab completion if:
246 246 # 1) The cursor is in the input buffer.
247 247 # 2) There is a non-whitespace character before the cursor.
248 248 text = self._get_input_buffer_cursor_line()
249 249 if text is None:
250 250 return False
251 251 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
252 252 if complete:
253 253 self._complete()
254 254 return not complete
255 255
256 256 #---------------------------------------------------------------------------
257 257 # 'ConsoleWidget' protected interface
258 258 #---------------------------------------------------------------------------
259 259
260 260 def _context_menu_make(self, pos):
261 261 """ Reimplemented to add an action for raw copy.
262 262 """
263 263 menu = super(FrontendWidget, self)._context_menu_make(pos)
264 264 for before_action in menu.actions():
265 265 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
266 266 QtGui.QKeySequence.ExactMatch:
267 267 menu.insertAction(before_action, self._copy_raw_action)
268 268 break
269 269 return menu
270 270
271 271 def request_interrupt_kernel(self):
272 272 if self._executing:
273 273 self.interrupt_kernel()
274 274
275 275 def request_restart_kernel(self):
276 276 message = 'Are you sure you want to restart the kernel?'
277 277 self.restart_kernel(message, now=False)
278 278
279 279 def _event_filter_console_keypress(self, event):
280 280 """ Reimplemented for execution interruption and smart backspace.
281 281 """
282 282 key = event.key()
283 283 if self._control_key_down(event.modifiers(), include_command=False):
284 284
285 285 if key == QtCore.Qt.Key_C and self._executing:
286 286 self.request_interrupt_kernel()
287 287 return True
288 288
289 289 elif key == QtCore.Qt.Key_Period:
290 290 self.request_restart_kernel()
291 291 return True
292 292
293 293 elif not event.modifiers() & QtCore.Qt.AltModifier:
294 294
295 295 # Smart backspace: remove four characters in one backspace if:
296 296 # 1) everything left of the cursor is whitespace
297 297 # 2) the four characters immediately left of the cursor are spaces
298 298 if key == QtCore.Qt.Key_Backspace:
299 299 col = self._get_input_buffer_cursor_column()
300 300 cursor = self._control.textCursor()
301 301 if col > 3 and not cursor.hasSelection():
302 302 text = self._get_input_buffer_cursor_line()[:col]
303 303 if text.endswith(' ') and not text.strip():
304 304 cursor.movePosition(QtGui.QTextCursor.Left,
305 305 QtGui.QTextCursor.KeepAnchor, 4)
306 306 cursor.removeSelectedText()
307 307 return True
308 308
309 309 return super(FrontendWidget, self)._event_filter_console_keypress(event)
310 310
311 311 def _insert_continuation_prompt(self, cursor):
312 312 """ Reimplemented for auto-indentation.
313 313 """
314 314 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
315 315 cursor.insertText(' ' * self._input_splitter.indent_spaces)
316 316
317 317 #---------------------------------------------------------------------------
318 318 # 'BaseFrontendMixin' abstract interface
319 319 #---------------------------------------------------------------------------
320 320
321 321 def _handle_complete_reply(self, rep):
322 322 """ Handle replies for tab completion.
323 323 """
324 324 self.log.debug("complete: %s", rep.get('content', ''))
325 325 cursor = self._get_cursor()
326 326 info = self._request_info.get('complete')
327 327 if info and info.id == rep['parent_header']['msg_id'] and \
328 328 info.pos == cursor.position():
329 329 text = '.'.join(self._get_context())
330 330 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
331 331 self._complete_with_items(cursor, rep['content']['matches'])
332 332
333 333 def _silent_exec_callback(self, expr, callback):
334 334 """Silently execute `expr` in the kernel and call `callback` with reply
335 335
336 336 the `expr` is evaluated silently in the kernel (without) output in
337 337 the frontend. Call `callback` with the
338 338 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
339 339
340 340 Parameters
341 341 ----------
342 342 expr : string
343 343 valid string to be executed by the kernel.
344 344 callback : function
345 345 function accepting one argument, as a string. The string will be
346 346 the `repr` of the result of evaluating `expr`
347 347
348 348 The `callback` is called with the `repr()` of the result of `expr` as
349 349 first argument. To get the object, do `eval()` on the passed value.
350 350
351 351 See Also
352 352 --------
353 353 _handle_exec_callback : private method, deal with calling callback with reply
354 354
355 355 """
356 356
357 357 # generate uuid, which would be used as an indication of whether or
358 358 # not the unique request originated from here (can use msg id ?)
359 359 local_uuid = str(uuid.uuid1())
360 360 msg_id = self.kernel_client.execute('',
361 361 silent=True, user_expressions={ local_uuid:expr })
362 362 self._callback_dict[local_uuid] = callback
363 363 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
364 364
365 365 def _handle_exec_callback(self, msg):
366 366 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
367 367
368 368 Parameters
369 369 ----------
370 370 msg : raw message send by the kernel containing an `user_expressions`
371 371 and having a 'silent_exec_callback' kind.
372 372
373 373 Notes
374 374 -----
375 375 This function will look for a `callback` associated with the
376 376 corresponding message id. Association has been made by
377 377 `_silent_exec_callback`. `callback` is then called with the `repr()`
378 378 of the value of corresponding `user_expressions` as argument.
379 379 `callback` is then removed from the known list so that any message
380 380 coming again with the same id won't trigger it.
381 381
382 382 """
383 383
384 384 user_exp = msg['content'].get('user_expressions')
385 385 if not user_exp:
386 386 return
387 387 for expression in user_exp:
388 388 if expression in self._callback_dict:
389 389 self._callback_dict.pop(expression)(user_exp[expression])
390 390
391 391 def _handle_execute_reply(self, msg):
392 392 """ Handles replies for code execution.
393 393 """
394 394 self.log.debug("execute: %s", msg.get('content', ''))
395 395 msg_id = msg['parent_header']['msg_id']
396 396 info = self._request_info['execute'].get(msg_id)
397 397 # unset reading flag, because if execute finished, raw_input can't
398 398 # still be pending.
399 399 self._reading = False
400 400 if info and info.kind == 'user' and not self._hidden:
401 401 # Make sure that all output from the SUB channel has been processed
402 402 # before writing a new prompt.
403 403 self.kernel_client.iopub_channel.flush()
404 404
405 405 # Reset the ANSI style information to prevent bad text in stdout
406 406 # from messing up our colors. We're not a true terminal so we're
407 407 # allowed to do this.
408 408 if self.ansi_codes:
409 409 self._ansi_processor.reset_sgr()
410 410
411 411 content = msg['content']
412 412 status = content['status']
413 413 if status == 'ok':
414 414 self._process_execute_ok(msg)
415 415 elif status == 'error':
416 416 self._process_execute_error(msg)
417 417 elif status == 'aborted':
418 418 self._process_execute_abort(msg)
419 419
420 420 self._show_interpreter_prompt_for_reply(msg)
421 421 self.executed.emit(msg)
422 422 self._request_info['execute'].pop(msg_id)
423 423 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
424 424 self._handle_exec_callback(msg)
425 425 self._request_info['execute'].pop(msg_id)
426 426 else:
427 427 super(FrontendWidget, self)._handle_execute_reply(msg)
428 428
429 429 def _handle_input_request(self, msg):
430 430 """ Handle requests for raw_input.
431 431 """
432 432 self.log.debug("input: %s", msg.get('content', ''))
433 433 if self._hidden:
434 434 raise RuntimeError('Request for raw input during hidden execution.')
435 435
436 436 # Make sure that all output from the SUB channel has been processed
437 437 # before entering readline mode.
438 438 self.kernel_client.iopub_channel.flush()
439 439
440 440 def callback(line):
441 441 self.kernel_client.stdin_channel.input(line)
442 442 if self._reading:
443 443 self.log.debug("Got second input request, assuming first was interrupted.")
444 444 self._reading = False
445 445 self._readline(msg['content']['prompt'], callback=callback)
446 446
447 447 def _kernel_restarted_message(self, died=True):
448 448 msg = "Kernel died, restarting" if died else "Kernel restarting"
449 449 self._append_html("<br>%s<hr><br>" % msg,
450 450 before_prompt=False
451 451 )
452 452
453 453 def _handle_kernel_died(self, since_last_heartbeat):
454 454 """Handle the kernel's death (if we do not own the kernel).
455 455 """
456 456 self.log.warn("kernel died: %s", since_last_heartbeat)
457 457 if self.custom_restart:
458 458 self.custom_restart_kernel_died.emit(since_last_heartbeat)
459 459 else:
460 460 self._kernel_restarted_message(died=True)
461 461 self.reset()
462 462
463 463 def _handle_kernel_restarted(self, died=True):
464 464 """Notice that the autorestarter restarted the kernel.
465 465
466 466 There's nothing to do but show a message.
467 467 """
468 468 self.log.warn("kernel restarted")
469 469 self._kernel_restarted_message(died=died)
470 470 self.reset()
471 471
472 472 def _handle_object_info_reply(self, rep):
473 473 """ Handle replies for call tips.
474 474 """
475 475 self.log.debug("oinfo: %s", rep.get('content', ''))
476 476 cursor = self._get_cursor()
477 477 info = self._request_info.get('call_tip')
478 478 if info and info.id == rep['parent_header']['msg_id'] and \
479 479 info.pos == cursor.position():
480 480 # Get the information for a call tip. For now we format the call
481 481 # line as string, later we can pass False to format_call and
482 482 # syntax-highlight it ourselves for nicer formatting in the
483 483 # calltip.
484 484 content = rep['content']
485 485 # if this is from pykernel, 'docstring' will be the only key
486 486 if content.get('ismagic', False):
487 487 # Don't generate a call-tip for magics. Ideally, we should
488 488 # generate a tooltip, but not on ( like we do for actual
489 489 # callables.
490 490 call_info, doc = None, None
491 491 else:
492 492 call_info, doc = call_tip(content, format_call=True)
493 493 if call_info or doc:
494 494 self._call_tip_widget.show_call_info(call_info, doc)
495 495
496 496 def _handle_pyout(self, msg):
497 497 """ Handle display hook output.
498 498 """
499 499 self.log.debug("pyout: %s", msg.get('content', ''))
500 500 if not self._hidden and self._is_from_this_session(msg):
501 501 text = msg['content']['data']
502 502 self._append_plain_text(text + '\n', before_prompt=True)
503 503
504 504 def _handle_stream(self, msg):
505 505 """ Handle stdout, stderr, and stdin.
506 506 """
507 507 self.log.debug("stream: %s", msg.get('content', ''))
508 508 if not self._hidden and self._is_from_this_session(msg):
509 509 # Most consoles treat tabs as being 8 space characters. Convert tabs
510 510 # to spaces so that output looks as expected regardless of this
511 511 # widget's tab width.
512 512 text = msg['content']['data'].expandtabs(8)
513 513
514 514 self._append_plain_text(text, before_prompt=True)
515 515 self._control.moveCursor(QtGui.QTextCursor.End)
516 516
517 517 def _handle_shutdown_reply(self, msg):
518 518 """ Handle shutdown signal, only if from other console.
519 519 """
520 520 self.log.warn("shutdown: %s", msg.get('content', ''))
521 521 restart = msg.get('content', {}).get('restart', False)
522 522 if not self._hidden and not self._is_from_this_session(msg):
523 523 # got shutdown reply, request came from session other than ours
524 524 if restart:
525 525 # someone restarted the kernel, handle it
526 526 self._handle_kernel_restarted(died=False)
527 527 else:
528 528 # kernel was shutdown permanently
529 529 # this triggers exit_requested if the kernel was local,
530 530 # and a dialog if the kernel was remote,
531 531 # so we don't suddenly clear the qtconsole without asking.
532 532 if self._local_kernel:
533 533 self.exit_requested.emit(self)
534 534 else:
535 535 title = self.window().windowTitle()
536 536 reply = QtGui.QMessageBox.question(self, title,
537 537 "Kernel has been shutdown permanently. "
538 538 "Close the Console?",
539 539 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
540 540 if reply == QtGui.QMessageBox.Yes:
541 541 self.exit_requested.emit(self)
542 542
543 543 def _handle_status(self, msg):
544 544 """Handle status message"""
545 545 # This is where a busy/idle indicator would be triggered,
546 546 # when we make one.
547 547 state = msg['content'].get('execution_state', '')
548 548 if state == 'starting':
549 549 # kernel started while we were running
550 550 if self._executing:
551 551 self._handle_kernel_restarted(died=True)
552 552 elif state == 'idle':
553 553 pass
554 554 elif state == 'busy':
555 555 pass
556 556
557 557 def _started_channels(self):
558 558 """ Called when the KernelManager channels have started listening or
559 559 when the frontend is assigned an already listening KernelManager.
560 560 """
561 561 self.reset(clear=True)
562 562
563 563 #---------------------------------------------------------------------------
564 564 # 'FrontendWidget' public interface
565 565 #---------------------------------------------------------------------------
566 566
567 567 def copy_raw(self):
568 568 """ Copy the currently selected text to the clipboard without attempting
569 569 to remove prompts or otherwise alter the text.
570 570 """
571 571 self._control.copy()
572 572
573 573 def execute_file(self, path, hidden=False):
574 574 """ Attempts to execute file with 'path'. If 'hidden', no output is
575 575 shown.
576 576 """
577 577 self.execute('execfile(%r)' % path, hidden=hidden)
578 578
579 579 def interrupt_kernel(self):
580 580 """ Attempts to interrupt the running kernel.
581 581
582 582 Also unsets _reading flag, to avoid runtime errors
583 583 if raw_input is called again.
584 584 """
585 585 if self.custom_interrupt:
586 586 self._reading = False
587 587 self.custom_interrupt_requested.emit()
588 588 elif self.kernel_manager:
589 589 self._reading = False
590 590 self.kernel_manager.interrupt_kernel()
591 591 else:
592 592 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
593 593
594 594 def reset(self, clear=False):
595 595 """ Resets the widget to its initial state if ``clear`` parameter
596 596 is True, otherwise
597 597 prints a visual indication of the fact that the kernel restarted, but
598 598 does not clear the traces from previous usage of the kernel before it
599 599 was restarted. With ``clear=True``, it is similar to ``%clear``, but
600 600 also re-writes the banner and aborts execution if necessary.
601 601 """
602 602 if self._executing:
603 603 self._executing = False
604 604 self._request_info['execute'] = {}
605 605 self._reading = False
606 606 self._highlighter.highlighting_on = False
607 607
608 608 if clear:
609 609 self._control.clear()
610 610 self._append_plain_text(self.banner)
611 611 # update output marker for stdout/stderr, so that startup
612 612 # messages appear after banner:
613 613 self._append_before_prompt_pos = self._get_cursor().position()
614 614 self._show_interpreter_prompt()
615 615
616 616 def restart_kernel(self, message, now=False):
617 617 """ Attempts to restart the running kernel.
618 618 """
619 619 # FIXME: now should be configurable via a checkbox in the dialog. Right
620 620 # now at least the heartbeat path sets it to True and the manual restart
621 621 # to False. But those should just be the pre-selected states of a
622 622 # checkbox that the user could override if so desired. But I don't know
623 623 # enough Qt to go implementing the checkbox now.
624 624
625 625 if self.custom_restart:
626 626 self.custom_restart_requested.emit()
627 627 return
628 628
629 629 if self.kernel_manager:
630 630 # Pause the heart beat channel to prevent further warnings.
631 631 self.kernel_client.hb_channel.pause()
632 632
633 633 # Prompt the user to restart the kernel. Un-pause the heartbeat if
634 634 # they decline. (If they accept, the heartbeat will be un-paused
635 635 # automatically when the kernel is restarted.)
636 636 if self.confirm_restart:
637 637 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
638 638 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
639 639 message, buttons)
640 640 do_restart = result == QtGui.QMessageBox.Yes
641 641 else:
642 642 # confirm_restart is False, so we don't need to ask user
643 643 # anything, just do the restart
644 644 do_restart = True
645 645 if do_restart:
646 646 try:
647 647 self.kernel_manager.restart_kernel(now=now)
648 648 except RuntimeError as e:
649 649 self._append_plain_text(
650 650 'Error restarting kernel: %s\n' % e,
651 651 before_prompt=True
652 652 )
653 653 else:
654 654 self._append_html("<br>Restarting kernel...\n<hr><br>",
655 655 before_prompt=True,
656 656 )
657 657 else:
658 658 self.kernel_client.hb_channel.unpause()
659 659
660 660 else:
661 661 self._append_plain_text(
662 662 'Cannot restart a Kernel I did not start\n',
663 663 before_prompt=True
664 664 )
665 665
666 666 #---------------------------------------------------------------------------
667 667 # 'FrontendWidget' protected interface
668 668 #---------------------------------------------------------------------------
669 669
670 670 def _call_tip(self):
671 671 """ Shows a call tip, if appropriate, at the current cursor location.
672 672 """
673 673 # Decide if it makes sense to show a call tip
674 674 if not self.enable_calltips:
675 675 return False
676 676 cursor = self._get_cursor()
677 677 cursor.movePosition(QtGui.QTextCursor.Left)
678 678 if cursor.document().characterAt(cursor.position()) != '(':
679 679 return False
680 680 context = self._get_context(cursor)
681 681 if not context:
682 682 return False
683 683
684 684 # Send the metadata request to the kernel
685 685 name = '.'.join(context)
686 686 msg_id = self.kernel_client.object_info(name)
687 687 pos = self._get_cursor().position()
688 688 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
689 689 return True
690 690
691 691 def _complete(self):
692 692 """ Performs completion at the current cursor location.
693 693 """
694 694 context = self._get_context()
695 695 if context:
696 696 # Send the completion request to the kernel
697 697 msg_id = self.kernel_client.complete(
698 698 '.'.join(context), # text
699 699 self._get_input_buffer_cursor_line(), # line
700 700 self._get_input_buffer_cursor_column(), # cursor_pos
701 701 self.input_buffer) # block
702 702 pos = self._get_cursor().position()
703 703 info = self._CompletionRequest(msg_id, pos)
704 704 self._request_info['complete'] = info
705 705
706 706 def _get_context(self, cursor=None):
707 707 """ Gets the context for the specified cursor (or the current cursor
708 708 if none is specified).
709 709 """
710 710 if cursor is None:
711 711 cursor = self._get_cursor()
712 712 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
713 713 QtGui.QTextCursor.KeepAnchor)
714 714 text = cursor.selection().toPlainText()
715 715 return self._completion_lexer.get_context(text)
716 716
717 717 def _process_execute_abort(self, msg):
718 718 """ Process a reply for an aborted execution request.
719 719 """
720 720 self._append_plain_text("ERROR: execution aborted\n")
721 721
722 722 def _process_execute_error(self, msg):
723 723 """ Process a reply for an execution request that resulted in an error.
724 724 """
725 725 content = msg['content']
726 726 # If a SystemExit is passed along, this means exit() was called - also
727 727 # all the ipython %exit magic syntax of '-k' to be used to keep
728 728 # the kernel running
729 729 if content['ename']=='SystemExit':
730 730 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
731 731 self._keep_kernel_on_exit = keepkernel
732 732 self.exit_requested.emit(self)
733 733 else:
734 734 traceback = ''.join(content['traceback'])
735 735 self._append_plain_text(traceback)
736 736
737 737 def _process_execute_ok(self, msg):
738 738 """ Process a reply for a successful execution request.
739 739 """
740 740 payload = msg['content']['payload']
741 741 for item in payload:
742 742 if not self._process_execute_payload(item):
743 743 warning = 'Warning: received unknown payload of type %s'
744 744 print(warning % repr(item['source']))
745 745
746 746 def _process_execute_payload(self, item):
747 747 """ Process a single payload item from the list of payload items in an
748 748 execution reply. Returns whether the payload was handled.
749 749 """
750 750 # The basic FrontendWidget doesn't handle payloads, as they are a
751 751 # mechanism for going beyond the standard Python interpreter model.
752 752 return False
753 753
754 754 def _show_interpreter_prompt(self):
755 755 """ Shows a prompt for the interpreter.
756 756 """
757 757 self._show_prompt('>>> ')
758 758
759 759 def _show_interpreter_prompt_for_reply(self, msg):
760 760 """ Shows a prompt for the interpreter given an 'execute_reply' message.
761 761 """
762 762 self._show_interpreter_prompt()
763 763
764 764 #------ Signal handlers ----------------------------------------------------
765 765
766 766 def _document_contents_change(self, position, removed, added):
767 767 """ Called whenever the document's content changes. Display a call tip
768 768 if appropriate.
769 769 """
770 770 # Calculate where the cursor should be *after* the change:
771 771 position += added
772 772
773 773 document = self._control.document()
774 774 if position == self._get_cursor().position():
775 775 self._call_tip()
776 776
777 777 #------ Trait default initializers -----------------------------------------
778 778
779 779 def _banner_default(self):
780 780 """ Returns the standard Python banner.
781 781 """
782 782 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
783 783 '"license" for more information.'
784 784 return banner % (sys.version, sys.platform)
@@ -1,304 +1,304 b''
1 1 # System library imports
2 2 from IPython.external.qt import QtGui
3 3
4 4 # Local imports
5 5 from IPython.utils.traitlets import Bool
6 from console_widget import ConsoleWidget
6 from .console_widget import ConsoleWidget
7 7
8 8
9 9 class HistoryConsoleWidget(ConsoleWidget):
10 10 """ A ConsoleWidget that keeps a history of the commands that have been
11 11 executed and provides a readline-esque interface to this history.
12 12 """
13 13
14 14 #------ Configuration ------------------------------------------------------
15 15
16 16 # If enabled, the input buffer will become "locked" to history movement when
17 17 # an edit is made to a multi-line input buffer. To override the lock, use
18 18 # Shift in conjunction with the standard history cycling keys.
19 19 history_lock = Bool(False, config=True)
20 20
21 21 #---------------------------------------------------------------------------
22 22 # 'object' interface
23 23 #---------------------------------------------------------------------------
24 24
25 25 def __init__(self, *args, **kw):
26 26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
27 27
28 28 # HistoryConsoleWidget protected variables.
29 29 self._history = []
30 30 self._history_edits = {}
31 31 self._history_index = 0
32 32 self._history_prefix = ''
33 33
34 34 #---------------------------------------------------------------------------
35 35 # 'ConsoleWidget' public interface
36 36 #---------------------------------------------------------------------------
37 37
38 38 def execute(self, source=None, hidden=False, interactive=False):
39 39 """ Reimplemented to the store history.
40 40 """
41 41 if not hidden:
42 42 history = self.input_buffer if source is None else source
43 43
44 44 executed = super(HistoryConsoleWidget, self).execute(
45 45 source, hidden, interactive)
46 46
47 47 if executed and not hidden:
48 48 # Save the command unless it was an empty string or was identical
49 49 # to the previous command.
50 50 history = history.rstrip()
51 51 if history and (not self._history or self._history[-1] != history):
52 52 self._history.append(history)
53 53
54 54 # Emulate readline: reset all history edits.
55 55 self._history_edits = {}
56 56
57 57 # Move the history index to the most recent item.
58 58 self._history_index = len(self._history)
59 59
60 60 return executed
61 61
62 62 #---------------------------------------------------------------------------
63 63 # 'ConsoleWidget' abstract interface
64 64 #---------------------------------------------------------------------------
65 65
66 66 def _up_pressed(self, shift_modifier):
67 67 """ Called when the up key is pressed. Returns whether to continue
68 68 processing the event.
69 69 """
70 70 prompt_cursor = self._get_prompt_cursor()
71 71 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
72 72 # Bail out if we're locked.
73 73 if self._history_locked() and not shift_modifier:
74 74 return False
75 75
76 76 # Set a search prefix based on the cursor position.
77 77 col = self._get_input_buffer_cursor_column()
78 78 input_buffer = self.input_buffer
79 79 # use the *shortest* of the cursor column and the history prefix
80 80 # to determine if the prefix has changed
81 81 n = min(col, len(self._history_prefix))
82 82
83 83 # prefix changed, restart search from the beginning
84 84 if (self._history_prefix[:n] != input_buffer[:n]):
85 85 self._history_index = len(self._history)
86 86
87 87 # the only time we shouldn't set the history prefix
88 88 # to the line up to the cursor is if we are already
89 89 # in a simple scroll (no prefix),
90 90 # and the cursor is at the end of the first line
91 91
92 92 # check if we are at the end of the first line
93 93 c = self._get_cursor()
94 94 current_pos = c.position()
95 95 c.movePosition(QtGui.QTextCursor.EndOfLine)
96 96 at_eol = (c.position() == current_pos)
97 97
98 98 if self._history_index == len(self._history) or \
99 99 not (self._history_prefix == '' and at_eol) or \
100 100 not (self._get_edited_history(self._history_index)[:col] == input_buffer[:col]):
101 101 self._history_prefix = input_buffer[:col]
102 102
103 103 # Perform the search.
104 104 self.history_previous(self._history_prefix,
105 105 as_prefix=not shift_modifier)
106 106
107 107 # Go to the first line of the prompt for seemless history scrolling.
108 108 # Emulate readline: keep the cursor position fixed for a prefix
109 109 # search.
110 110 cursor = self._get_prompt_cursor()
111 111 if self._history_prefix:
112 112 cursor.movePosition(QtGui.QTextCursor.Right,
113 113 n=len(self._history_prefix))
114 114 else:
115 115 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
116 116 self._set_cursor(cursor)
117 117
118 118 return False
119 119
120 120 return True
121 121
122 122 def _down_pressed(self, shift_modifier):
123 123 """ Called when the down key is pressed. Returns whether to continue
124 124 processing the event.
125 125 """
126 126 end_cursor = self._get_end_cursor()
127 127 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
128 128 # Bail out if we're locked.
129 129 if self._history_locked() and not shift_modifier:
130 130 return False
131 131
132 132 # Perform the search.
133 133 replaced = self.history_next(self._history_prefix,
134 134 as_prefix=not shift_modifier)
135 135
136 136 # Emulate readline: keep the cursor position fixed for a prefix
137 137 # search. (We don't need to move the cursor to the end of the buffer
138 138 # in the other case because this happens automatically when the
139 139 # input buffer is set.)
140 140 if self._history_prefix and replaced:
141 141 cursor = self._get_prompt_cursor()
142 142 cursor.movePosition(QtGui.QTextCursor.Right,
143 143 n=len(self._history_prefix))
144 144 self._set_cursor(cursor)
145 145
146 146 return False
147 147
148 148 return True
149 149
150 150 #---------------------------------------------------------------------------
151 151 # 'HistoryConsoleWidget' public interface
152 152 #---------------------------------------------------------------------------
153 153
154 154 def history_previous(self, substring='', as_prefix=True):
155 155 """ If possible, set the input buffer to a previous history item.
156 156
157 157 Parameters:
158 158 -----------
159 159 substring : str, optional
160 160 If specified, search for an item with this substring.
161 161 as_prefix : bool, optional
162 162 If True, the substring must match at the beginning (default).
163 163
164 164 Returns:
165 165 --------
166 166 Whether the input buffer was changed.
167 167 """
168 168 index = self._history_index
169 169 replace = False
170 170 while index > 0:
171 171 index -= 1
172 172 history = self._get_edited_history(index)
173 173 if (as_prefix and history.startswith(substring)) \
174 174 or (not as_prefix and substring in history):
175 175 replace = True
176 176 break
177 177
178 178 if replace:
179 179 self._store_edits()
180 180 self._history_index = index
181 181 self.input_buffer = history
182 182
183 183 return replace
184 184
185 185 def history_next(self, substring='', as_prefix=True):
186 186 """ If possible, set the input buffer to a subsequent history item.
187 187
188 188 Parameters:
189 189 -----------
190 190 substring : str, optional
191 191 If specified, search for an item with this substring.
192 192 as_prefix : bool, optional
193 193 If True, the substring must match at the beginning (default).
194 194
195 195 Returns:
196 196 --------
197 197 Whether the input buffer was changed.
198 198 """
199 199 index = self._history_index
200 200 replace = False
201 201 while index < len(self._history):
202 202 index += 1
203 203 history = self._get_edited_history(index)
204 204 if (as_prefix and history.startswith(substring)) \
205 205 or (not as_prefix and substring in history):
206 206 replace = True
207 207 break
208 208
209 209 if replace:
210 210 self._store_edits()
211 211 self._history_index = index
212 212 self.input_buffer = history
213 213
214 214 return replace
215 215
216 216 def history_tail(self, n=10):
217 217 """ Get the local history list.
218 218
219 219 Parameters:
220 220 -----------
221 221 n : int
222 222 The (maximum) number of history items to get.
223 223 """
224 224 return self._history[-n:]
225 225
226 226 def _request_update_session_history_length(self):
227 227 msg_id = self.kernel_client.shell_channel.execute('',
228 228 silent=True,
229 229 user_expressions={
230 230 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
231 231 }
232 232 )
233 233 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'save_magic')
234 234
235 235 def _handle_execute_reply(self, msg):
236 236 """ Handles replies for code execution, here only session history length
237 237 """
238 238 msg_id = msg['parent_header']['msg_id']
239 239 info = self._request_info['execute'].pop(msg_id,None)
240 240 if info and info.kind == 'save_magic' and not self._hidden:
241 241 content = msg['content']
242 242 status = content['status']
243 243 if status == 'ok':
244 244 self._max_session_history = int(
245 245 content['user_expressions']['hlen']['data']['text/plain']
246 246 )
247 247
248 248 def save_magic(self):
249 249 # update the session history length
250 250 self._request_update_session_history_length()
251 251
252 252 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
253 253 "Enter A filename",
254 254 filter='Python File (*.py);; All files (*.*)'
255 255 )
256 256
257 257 # let's the user search/type for a file name, while the history length
258 258 # is fetched
259 259
260 260 if file_name:
261 261 hist_range, ok = QtGui.QInputDialog.getText(self,
262 262 'Please enter an interval of command to save',
263 263 'Saving commands:',
264 264 text=str('1-'+str(self._max_session_history))
265 265 )
266 266 if ok:
267 267 self.execute("%save"+" "+file_name+" "+str(hist_range))
268 268
269 269 #---------------------------------------------------------------------------
270 270 # 'HistoryConsoleWidget' protected interface
271 271 #---------------------------------------------------------------------------
272 272
273 273 def _history_locked(self):
274 274 """ Returns whether history movement is locked.
275 275 """
276 276 return (self.history_lock and
277 277 (self._get_edited_history(self._history_index) !=
278 278 self.input_buffer) and
279 279 (self._get_prompt_cursor().blockNumber() !=
280 280 self._get_end_cursor().blockNumber()))
281 281
282 282 def _get_edited_history(self, index):
283 283 """ Retrieves a history item, possibly with temporary edits.
284 284 """
285 285 if index in self._history_edits:
286 286 return self._history_edits[index]
287 287 elif index == len(self._history):
288 288 return unicode()
289 289 return self._history[index]
290 290
291 291 def _set_history(self, history):
292 292 """ Replace the current history with a sequence of history items.
293 293 """
294 294 self._history = list(history)
295 295 self._history_edits = {}
296 296 self._history_index = len(self._history)
297 297
298 298 def _store_edits(self):
299 299 """ If there are edits to the current input buffer, store them.
300 300 """
301 301 current = self.input_buffer
302 302 if self._history_index == len(self._history) or \
303 303 self._history[self._history_index] != current:
304 304 self._history_edits[self._history_index] = current
@@ -1,584 +1,584 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 import time
16 16 from textwrap import dedent
17 17
18 18 # System library imports
19 19 from IPython.external.qt import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter
23 23 from IPython.core.inputtransformer import ipy_prompt
24 24 from IPython.utils.traitlets import Bool, Unicode
25 from frontend_widget import FrontendWidget
26 import styles
25 from .frontend_widget import FrontendWidget
26 from . import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 65 {filename} format specifier, it will be used. Otherwise, the filename
66 66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 73 this parameter is not specified, the line number option to the %edit
74 74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85 syntax_style = Unicode(config=True,
86 86 help="""
87 87 If not empty, use this Pygments style for syntax highlighting.
88 88 Otherwise, the style sheet is queried for Pygments style
89 89 information.
90 90 """)
91 91
92 92 # Prompts.
93 93 in_prompt = Unicode(default_in_prompt, config=True)
94 94 out_prompt = Unicode(default_out_prompt, config=True)
95 95 input_sep = Unicode(default_input_sep, config=True)
96 96 output_sep = Unicode(default_output_sep, config=True)
97 97 output_sep2 = Unicode(default_output_sep2, config=True)
98 98
99 99 # FrontendWidget protected class variables.
100 100 _input_splitter_class = IPythonInputSplitter
101 101 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
102 102 logical_line_transforms=[],
103 103 python_line_transforms=[],
104 104 )
105 105
106 106 # IPythonWidget protected class variables.
107 107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
108 108 _payload_source_edit = 'edit_magic'
109 109 _payload_source_exit = 'ask_exit'
110 110 _payload_source_next_input = 'set_next_input'
111 111 _payload_source_page = 'page'
112 112 _retrying_history_request = False
113 113
114 114 #---------------------------------------------------------------------------
115 115 # 'object' interface
116 116 #---------------------------------------------------------------------------
117 117
118 118 def __init__(self, *args, **kw):
119 119 super(IPythonWidget, self).__init__(*args, **kw)
120 120
121 121 # IPythonWidget protected variables.
122 122 self._payload_handlers = {
123 123 self._payload_source_edit : self._handle_payload_edit,
124 124 self._payload_source_exit : self._handle_payload_exit,
125 125 self._payload_source_page : self._handle_payload_page,
126 126 self._payload_source_next_input : self._handle_payload_next_input }
127 127 self._previous_prompt_obj = None
128 128 self._keep_kernel_on_exit = None
129 129
130 130 # Initialize widget styling.
131 131 if self.style_sheet:
132 132 self._style_sheet_changed()
133 133 self._syntax_style_changed()
134 134 else:
135 135 self.set_default_style()
136 136
137 137 #---------------------------------------------------------------------------
138 138 # 'BaseFrontendMixin' abstract interface
139 139 #---------------------------------------------------------------------------
140 140
141 141 def _handle_complete_reply(self, rep):
142 142 """ Reimplemented to support IPython's improved completion machinery.
143 143 """
144 144 self.log.debug("complete: %s", rep.get('content', ''))
145 145 cursor = self._get_cursor()
146 146 info = self._request_info.get('complete')
147 147 if info and info.id == rep['parent_header']['msg_id'] and \
148 148 info.pos == cursor.position():
149 149 matches = rep['content']['matches']
150 150 text = rep['content']['matched_text']
151 151 offset = len(text)
152 152
153 153 # Clean up matches with period and path separators if the matched
154 154 # text has not been transformed. This is done by truncating all
155 155 # but the last component and then suitably decreasing the offset
156 156 # between the current cursor position and the start of completion.
157 157 if len(matches) > 1 and matches[0][:offset] == text:
158 158 parts = re.split(r'[./\\]', text)
159 159 sep_count = len(parts) - 1
160 160 if sep_count:
161 161 chop_length = sum(map(len, parts[:sep_count])) + sep_count
162 162 matches = [ match[chop_length:] for match in matches ]
163 163 offset -= chop_length
164 164
165 165 # Move the cursor to the start of the match and complete.
166 166 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
167 167 self._complete_with_items(cursor, matches)
168 168
169 169 def _handle_execute_reply(self, msg):
170 170 """ Reimplemented to support prompt requests.
171 171 """
172 172 msg_id = msg['parent_header'].get('msg_id')
173 173 info = self._request_info['execute'].get(msg_id)
174 174 if info and info.kind == 'prompt':
175 175 number = msg['content']['execution_count'] + 1
176 176 self._show_interpreter_prompt(number)
177 177 self._request_info['execute'].pop(msg_id)
178 178 else:
179 179 super(IPythonWidget, self)._handle_execute_reply(msg)
180 180
181 181 def _handle_history_reply(self, msg):
182 182 """ Implemented to handle history tail replies, which are only supported
183 183 by the IPython kernel.
184 184 """
185 185 content = msg['content']
186 186 if 'history' not in content:
187 187 self.log.error("History request failed: %r"%content)
188 188 if content.get('status', '') == 'aborted' and \
189 189 not self._retrying_history_request:
190 190 # a *different* action caused this request to be aborted, so
191 191 # we should try again.
192 192 self.log.error("Retrying aborted history request")
193 193 # prevent multiple retries of aborted requests:
194 194 self._retrying_history_request = True
195 195 # wait out the kernel's queue flush, which is currently timed at 0.1s
196 196 time.sleep(0.25)
197 197 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
198 198 else:
199 199 self._retrying_history_request = False
200 200 return
201 201 # reset retry flag
202 202 self._retrying_history_request = False
203 203 history_items = content['history']
204 204 self.log.debug("Received history reply with %i entries", len(history_items))
205 205 items = []
206 206 last_cell = u""
207 207 for _, _, cell in history_items:
208 208 cell = cell.rstrip()
209 209 if cell != last_cell:
210 210 items.append(cell)
211 211 last_cell = cell
212 212 self._set_history(items)
213 213
214 214 def _handle_pyout(self, msg):
215 215 """ Reimplemented for IPython-style "display hook".
216 216 """
217 217 self.log.debug("pyout: %s", msg.get('content', ''))
218 218 if not self._hidden and self._is_from_this_session(msg):
219 219 content = msg['content']
220 220 prompt_number = content.get('execution_count', 0)
221 221 data = content['data']
222 222 if 'text/html' in data:
223 223 self._append_plain_text(self.output_sep, True)
224 224 self._append_html(self._make_out_prompt(prompt_number), True)
225 225 html = data['text/html']
226 226 self._append_plain_text('\n', True)
227 227 self._append_html(html + self.output_sep2, True)
228 228 elif 'text/plain' in data:
229 229 self._append_plain_text(self.output_sep, True)
230 230 self._append_html(self._make_out_prompt(prompt_number), True)
231 231 text = data['text/plain']
232 232 # If the repr is multiline, make sure we start on a new line,
233 233 # so that its lines are aligned.
234 234 if "\n" in text and not self.output_sep.endswith("\n"):
235 235 self._append_plain_text('\n', True)
236 236 self._append_plain_text(text + self.output_sep2, True)
237 237
238 238 def _handle_display_data(self, msg):
239 239 """ The base handler for the ``display_data`` message.
240 240 """
241 241 self.log.debug("display: %s", msg.get('content', ''))
242 242 # For now, we don't display data from other frontends, but we
243 243 # eventually will as this allows all frontends to monitor the display
244 244 # data. But we need to figure out how to handle this in the GUI.
245 245 if not self._hidden and self._is_from_this_session(msg):
246 246 source = msg['content']['source']
247 247 data = msg['content']['data']
248 248 metadata = msg['content']['metadata']
249 249 # In the regular IPythonWidget, we simply print the plain text
250 250 # representation.
251 251 if 'text/html' in data:
252 252 html = data['text/html']
253 253 self._append_html(html, True)
254 254 elif 'text/plain' in data:
255 255 text = data['text/plain']
256 256 self._append_plain_text(text, True)
257 257 # This newline seems to be needed for text and html output.
258 258 self._append_plain_text(u'\n', True)
259 259
260 260 def _started_channels(self):
261 261 """Reimplemented to make a history request and load %guiref."""
262 262 super(IPythonWidget, self)._started_channels()
263 263 self._load_guiref_magic()
264 264 self.kernel_client.shell_channel.history(hist_access_type='tail',
265 265 n=1000)
266 266
267 267 def _started_kernel(self):
268 268 """Load %guiref when the kernel starts (if channels are also started).
269 269
270 270 Principally triggered by kernel restart.
271 271 """
272 272 if self.kernel_client.shell_channel is not None:
273 273 self._load_guiref_magic()
274 274
275 275 def _load_guiref_magic(self):
276 276 """Load %guiref magic."""
277 277 self.kernel_client.shell_channel.execute('\n'.join([
278 278 "try:",
279 279 " _usage",
280 280 "except:",
281 281 " from IPython.core import usage as _usage",
282 282 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
283 283 " del _usage",
284 284 ]), silent=True)
285 285
286 286 #---------------------------------------------------------------------------
287 287 # 'ConsoleWidget' public interface
288 288 #---------------------------------------------------------------------------
289 289
290 290 #---------------------------------------------------------------------------
291 291 # 'FrontendWidget' public interface
292 292 #---------------------------------------------------------------------------
293 293
294 294 def execute_file(self, path, hidden=False):
295 295 """ Reimplemented to use the 'run' magic.
296 296 """
297 297 # Use forward slashes on Windows to avoid escaping each separator.
298 298 if sys.platform == 'win32':
299 299 path = os.path.normpath(path).replace('\\', '/')
300 300
301 301 # Perhaps we should not be using %run directly, but while we
302 302 # are, it is necessary to quote or escape filenames containing spaces
303 303 # or quotes.
304 304
305 305 # In earlier code here, to minimize escaping, we sometimes quoted the
306 306 # filename with single quotes. But to do this, this code must be
307 307 # platform-aware, because run uses shlex rather than python string
308 308 # parsing, so that:
309 309 # * In Win: single quotes can be used in the filename without quoting,
310 310 # and we cannot use single quotes to quote the filename.
311 311 # * In *nix: we can escape double quotes in a double quoted filename,
312 312 # but can't escape single quotes in a single quoted filename.
313 313
314 314 # So to keep this code non-platform-specific and simple, we now only
315 315 # use double quotes to quote filenames, and escape when needed:
316 316 if ' ' in path or "'" in path or '"' in path:
317 317 path = '"%s"' % path.replace('"', '\\"')
318 318 self.execute('%%run %s' % path, hidden=hidden)
319 319
320 320 #---------------------------------------------------------------------------
321 321 # 'FrontendWidget' protected interface
322 322 #---------------------------------------------------------------------------
323 323
324 324 def _complete(self):
325 325 """ Reimplemented to support IPython's improved completion machinery.
326 326 """
327 327 # We let the kernel split the input line, so we *always* send an empty
328 328 # text field. Readline-based frontends do get a real text field which
329 329 # they can use.
330 330 text = ''
331 331
332 332 # Send the completion request to the kernel
333 333 msg_id = self.kernel_client.shell_channel.complete(
334 334 text, # text
335 335 self._get_input_buffer_cursor_line(), # line
336 336 self._get_input_buffer_cursor_column(), # cursor_pos
337 337 self.input_buffer) # block
338 338 pos = self._get_cursor().position()
339 339 info = self._CompletionRequest(msg_id, pos)
340 340 self._request_info['complete'] = info
341 341
342 342 def _process_execute_error(self, msg):
343 343 """ Reimplemented for IPython-style traceback formatting.
344 344 """
345 345 content = msg['content']
346 346 traceback = '\n'.join(content['traceback']) + '\n'
347 347 if False:
348 348 # FIXME: For now, tracebacks come as plain text, so we can't use
349 349 # the html renderer yet. Once we refactor ultratb to produce
350 350 # properly styled tracebacks, this branch should be the default
351 351 traceback = traceback.replace(' ', '&nbsp;')
352 352 traceback = traceback.replace('\n', '<br/>')
353 353
354 354 ename = content['ename']
355 355 ename_styled = '<span class="error">%s</span>' % ename
356 356 traceback = traceback.replace(ename, ename_styled)
357 357
358 358 self._append_html(traceback)
359 359 else:
360 360 # This is the fallback for now, using plain text with ansi escapes
361 361 self._append_plain_text(traceback)
362 362
363 363 def _process_execute_payload(self, item):
364 364 """ Reimplemented to dispatch payloads to handler methods.
365 365 """
366 366 handler = self._payload_handlers.get(item['source'])
367 367 if handler is None:
368 368 # We have no handler for this type of payload, simply ignore it
369 369 return False
370 370 else:
371 371 handler(item)
372 372 return True
373 373
374 374 def _show_interpreter_prompt(self, number=None):
375 375 """ Reimplemented for IPython-style prompts.
376 376 """
377 377 # If a number was not specified, make a prompt number request.
378 378 if number is None:
379 379 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
380 380 info = self._ExecutionRequest(msg_id, 'prompt')
381 381 self._request_info['execute'][msg_id] = info
382 382 return
383 383
384 384 # Show a new prompt and save information about it so that it can be
385 385 # updated later if the prompt number turns out to be wrong.
386 386 self._prompt_sep = self.input_sep
387 387 self._show_prompt(self._make_in_prompt(number), html=True)
388 388 block = self._control.document().lastBlock()
389 389 length = len(self._prompt)
390 390 self._previous_prompt_obj = self._PromptBlock(block, length, number)
391 391
392 392 # Update continuation prompt to reflect (possibly) new prompt length.
393 393 self._set_continuation_prompt(
394 394 self._make_continuation_prompt(self._prompt), html=True)
395 395
396 396 def _show_interpreter_prompt_for_reply(self, msg):
397 397 """ Reimplemented for IPython-style prompts.
398 398 """
399 399 # Update the old prompt number if necessary.
400 400 content = msg['content']
401 401 # abort replies do not have any keys:
402 402 if content['status'] == 'aborted':
403 403 if self._previous_prompt_obj:
404 404 previous_prompt_number = self._previous_prompt_obj.number
405 405 else:
406 406 previous_prompt_number = 0
407 407 else:
408 408 previous_prompt_number = content['execution_count']
409 409 if self._previous_prompt_obj and \
410 410 self._previous_prompt_obj.number != previous_prompt_number:
411 411 block = self._previous_prompt_obj.block
412 412
413 413 # Make sure the prompt block has not been erased.
414 414 if block.isValid() and block.text():
415 415
416 416 # Remove the old prompt and insert a new prompt.
417 417 cursor = QtGui.QTextCursor(block)
418 418 cursor.movePosition(QtGui.QTextCursor.Right,
419 419 QtGui.QTextCursor.KeepAnchor,
420 420 self._previous_prompt_obj.length)
421 421 prompt = self._make_in_prompt(previous_prompt_number)
422 422 self._prompt = self._insert_html_fetching_plain_text(
423 423 cursor, prompt)
424 424
425 425 # When the HTML is inserted, Qt blows away the syntax
426 426 # highlighting for the line, so we need to rehighlight it.
427 427 self._highlighter.rehighlightBlock(cursor.block())
428 428
429 429 self._previous_prompt_obj = None
430 430
431 431 # Show a new prompt with the kernel's estimated prompt number.
432 432 self._show_interpreter_prompt(previous_prompt_number + 1)
433 433
434 434 #---------------------------------------------------------------------------
435 435 # 'IPythonWidget' interface
436 436 #---------------------------------------------------------------------------
437 437
438 438 def set_default_style(self, colors='lightbg'):
439 439 """ Sets the widget style to the class defaults.
440 440
441 441 Parameters:
442 442 -----------
443 443 colors : str, optional (default lightbg)
444 444 Whether to use the default IPython light background or dark
445 445 background or B&W style.
446 446 """
447 447 colors = colors.lower()
448 448 if colors=='lightbg':
449 449 self.style_sheet = styles.default_light_style_sheet
450 450 self.syntax_style = styles.default_light_syntax_style
451 451 elif colors=='linux':
452 452 self.style_sheet = styles.default_dark_style_sheet
453 453 self.syntax_style = styles.default_dark_syntax_style
454 454 elif colors=='nocolor':
455 455 self.style_sheet = styles.default_bw_style_sheet
456 456 self.syntax_style = styles.default_bw_syntax_style
457 457 else:
458 458 raise KeyError("No such color scheme: %s"%colors)
459 459
460 460 #---------------------------------------------------------------------------
461 461 # 'IPythonWidget' protected interface
462 462 #---------------------------------------------------------------------------
463 463
464 464 def _edit(self, filename, line=None):
465 465 """ Opens a Python script for editing.
466 466
467 467 Parameters:
468 468 -----------
469 469 filename : str
470 470 A path to a local system file.
471 471
472 472 line : int, optional
473 473 A line of interest in the file.
474 474 """
475 475 if self.custom_edit:
476 476 self.custom_edit_requested.emit(filename, line)
477 477 elif not self.editor:
478 478 self._append_plain_text('No default editor available.\n'
479 479 'Specify a GUI text editor in the `IPythonWidget.editor` '
480 480 'configurable to enable the %edit magic')
481 481 else:
482 482 try:
483 483 filename = '"%s"' % filename
484 484 if line and self.editor_line:
485 485 command = self.editor_line.format(filename=filename,
486 486 line=line)
487 487 else:
488 488 try:
489 489 command = self.editor.format()
490 490 except KeyError:
491 491 command = self.editor.format(filename=filename)
492 492 else:
493 493 command += ' ' + filename
494 494 except KeyError:
495 495 self._append_plain_text('Invalid editor command.\n')
496 496 else:
497 497 try:
498 498 Popen(command, shell=True)
499 499 except OSError:
500 500 msg = 'Opening editor with command "%s" failed.\n'
501 501 self._append_plain_text(msg % command)
502 502
503 503 def _make_in_prompt(self, number):
504 504 """ Given a prompt number, returns an HTML In prompt.
505 505 """
506 506 try:
507 507 body = self.in_prompt % number
508 508 except TypeError:
509 509 # allow in_prompt to leave out number, e.g. '>>> '
510 510 body = self.in_prompt
511 511 return '<span class="in-prompt">%s</span>' % body
512 512
513 513 def _make_continuation_prompt(self, prompt):
514 514 """ Given a plain text version of an In prompt, returns an HTML
515 515 continuation prompt.
516 516 """
517 517 end_chars = '...: '
518 518 space_count = len(prompt.lstrip('\n')) - len(end_chars)
519 519 body = '&nbsp;' * space_count + end_chars
520 520 return '<span class="in-prompt">%s</span>' % body
521 521
522 522 def _make_out_prompt(self, number):
523 523 """ Given a prompt number, returns an HTML Out prompt.
524 524 """
525 525 body = self.out_prompt % number
526 526 return '<span class="out-prompt">%s</span>' % body
527 527
528 528 #------ Payload handlers --------------------------------------------------
529 529
530 530 # Payload handlers with a generic interface: each takes the opaque payload
531 531 # dict, unpacks it and calls the underlying functions with the necessary
532 532 # arguments.
533 533
534 534 def _handle_payload_edit(self, item):
535 535 self._edit(item['filename'], item['line_number'])
536 536
537 537 def _handle_payload_exit(self, item):
538 538 self._keep_kernel_on_exit = item['keepkernel']
539 539 self.exit_requested.emit(self)
540 540
541 541 def _handle_payload_next_input(self, item):
542 542 self.input_buffer = dedent(item['text'].rstrip())
543 543
544 544 def _handle_payload_page(self, item):
545 545 # Since the plain text widget supports only a very small subset of HTML
546 546 # and we have no control over the HTML source, we only page HTML
547 547 # payloads in the rich text widget.
548 548 if item['html'] and self.kind == 'rich':
549 549 self._page(item['html'], html=True)
550 550 else:
551 551 self._page(item['text'], html=False)
552 552
553 553 #------ Trait change handlers --------------------------------------------
554 554
555 555 def _style_sheet_changed(self):
556 556 """ Set the style sheets of the underlying widgets.
557 557 """
558 558 self.setStyleSheet(self.style_sheet)
559 559 if self._control is not None:
560 560 self._control.document().setDefaultStyleSheet(self.style_sheet)
561 561 bg_color = self._control.palette().window().color()
562 562 self._ansi_processor.set_background_color(bg_color)
563 563
564 564 if self._page_control is not None:
565 565 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
566 566
567 567
568 568
569 569 def _syntax_style_changed(self):
570 570 """ Set the style for the syntax highlighter.
571 571 """
572 572 if self._highlighter is None:
573 573 # ignore premature calls
574 574 return
575 575 if self.syntax_style:
576 576 self._highlighter.set_style(self.syntax_style)
577 577 else:
578 578 self._highlighter.set_style_sheet(self.style_sheet)
579 579
580 580 #------ Trait default initializers -----------------------------------------
581 581
582 582 def _banner_default(self):
583 583 from IPython.core.usage import default_gui_banner
584 584 return default_gui_banner
@@ -1,341 +1,341 b''
1 1 #-----------------------------------------------------------------------------
2 2 # Copyright (c) 2010, IPython Development Team.
3 3 #
4 4 # Distributed under the terms of the Modified BSD License.
5 5 #
6 6 # The full license is in the file COPYING.txt, distributed with this software.
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard libary imports.
10 10 from base64 import decodestring
11 11 import os
12 12 import re
13 13
14 14 # System libary imports.
15 15 from IPython.external.qt import QtCore, QtGui
16 16
17 17 # Local imports
18 18 from IPython.utils.traitlets import Bool
19 19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 from ipython_widget import IPythonWidget
20 from .ipython_widget import IPythonWidget
21 21
22 22
23 23 class RichIPythonWidget(IPythonWidget):
24 24 """ An IPythonWidget that supports rich text, including lists, images, and
25 25 tables. Note that raw performance will be reduced compared to the plain
26 26 text version.
27 27 """
28 28
29 29 # RichIPythonWidget protected class variables.
30 30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
31 31 _jpg_supported = Bool(False)
32 32
33 33 # Used to determine whether a given html export attempt has already
34 34 # displayed a warning about being unable to convert a png to svg.
35 35 _svg_warning_displayed = False
36 36
37 37 #---------------------------------------------------------------------------
38 38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 41 def __init__(self, *args, **kw):
42 42 """ Create a RichIPythonWidget.
43 43 """
44 44 kw['kind'] = 'rich'
45 45 super(RichIPythonWidget, self).__init__(*args, **kw)
46 46
47 47 # Configure the ConsoleWidget HTML exporter for our formats.
48 48 self._html_exporter.image_tag = self._get_image_tag
49 49
50 50 # Dictionary for resolving document resource names to SVG data.
51 51 self._name_to_svg_map = {}
52 52
53 53 # Do we support jpg ?
54 54 # it seems that sometime jpg support is a plugin of QT, so try to assume
55 55 # it is not always supported.
56 56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
57 57 self._jpg_supported = 'jpeg' in _supported_format
58 58
59 59
60 60 #---------------------------------------------------------------------------
61 61 # 'ConsoleWidget' public interface overides
62 62 #---------------------------------------------------------------------------
63 63
64 64 def export_html(self):
65 65 """ Shows a dialog to export HTML/XML in various formats.
66 66
67 67 Overridden in order to reset the _svg_warning_displayed flag prior
68 68 to the export running.
69 69 """
70 70 self._svg_warning_displayed = False
71 71 super(RichIPythonWidget, self).export_html()
72 72
73 73
74 74 #---------------------------------------------------------------------------
75 75 # 'ConsoleWidget' protected interface
76 76 #---------------------------------------------------------------------------
77 77
78 78 def _context_menu_make(self, pos):
79 79 """ Reimplemented to return a custom context menu for images.
80 80 """
81 81 format = self._control.cursorForPosition(pos).charFormat()
82 82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
83 83 if name:
84 84 menu = QtGui.QMenu()
85 85
86 86 menu.addAction('Copy Image', lambda: self._copy_image(name))
87 87 menu.addAction('Save Image As...', lambda: self._save_image(name))
88 88 menu.addSeparator()
89 89
90 90 svg = self._name_to_svg_map.get(name, None)
91 91 if svg is not None:
92 92 menu.addSeparator()
93 93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
94 94 menu.addAction('Save SVG As...',
95 95 lambda: save_svg(svg, self._control))
96 96 else:
97 97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
98 98 return menu
99 99
100 100 #---------------------------------------------------------------------------
101 101 # 'BaseFrontendMixin' abstract interface
102 102 #---------------------------------------------------------------------------
103 103 def _pre_image_append(self, msg, prompt_number):
104 104 """ Append the Out[] prompt and make the output nicer
105 105
106 106 Shared code for some the following if statement
107 107 """
108 108 self.log.debug("pyout: %s", msg.get('content', ''))
109 109 self._append_plain_text(self.output_sep, True)
110 110 self._append_html(self._make_out_prompt(prompt_number), True)
111 111 self._append_plain_text('\n', True)
112 112
113 113 def _handle_pyout(self, msg):
114 114 """ Overridden to handle rich data types, like SVG.
115 115 """
116 116 if not self._hidden and self._is_from_this_session(msg):
117 117 content = msg['content']
118 118 prompt_number = content.get('execution_count', 0)
119 119 data = content['data']
120 120 metadata = msg['content']['metadata']
121 121 if 'image/svg+xml' in data:
122 122 self._pre_image_append(msg, prompt_number)
123 123 self._append_svg(data['image/svg+xml'], True)
124 124 self._append_html(self.output_sep2, True)
125 125 elif 'image/png' in data:
126 126 self._pre_image_append(msg, prompt_number)
127 127 png = decodestring(data['image/png'].encode('ascii'))
128 128 self._append_png(png, True, metadata=metadata.get('image/png', None))
129 129 self._append_html(self.output_sep2, True)
130 130 elif 'image/jpeg' in data and self._jpg_supported:
131 131 self._pre_image_append(msg, prompt_number)
132 132 jpg = decodestring(data['image/jpeg'].encode('ascii'))
133 133 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
134 134 self._append_html(self.output_sep2, True)
135 135 else:
136 136 # Default back to the plain text representation.
137 137 return super(RichIPythonWidget, self)._handle_pyout(msg)
138 138
139 139 def _handle_display_data(self, msg):
140 140 """ Overridden to handle rich data types, like SVG.
141 141 """
142 142 if not self._hidden and self._is_from_this_session(msg):
143 143 source = msg['content']['source']
144 144 data = msg['content']['data']
145 145 metadata = msg['content']['metadata']
146 146 # Try to use the svg or html representations.
147 147 # FIXME: Is this the right ordering of things to try?
148 148 if 'image/svg+xml' in data:
149 149 self.log.debug("display: %s", msg.get('content', ''))
150 150 svg = data['image/svg+xml']
151 151 self._append_svg(svg, True)
152 152 elif 'image/png' in data:
153 153 self.log.debug("display: %s", msg.get('content', ''))
154 154 # PNG data is base64 encoded as it passes over the network
155 155 # in a JSON structure so we decode it.
156 156 png = decodestring(data['image/png'].encode('ascii'))
157 157 self._append_png(png, True, metadata=metadata.get('image/png', None))
158 158 elif 'image/jpeg' in data and self._jpg_supported:
159 159 self.log.debug("display: %s", msg.get('content', ''))
160 160 jpg = decodestring(data['image/jpeg'].encode('ascii'))
161 161 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
162 162 else:
163 163 # Default back to the plain text representation.
164 164 return super(RichIPythonWidget, self)._handle_display_data(msg)
165 165
166 166 #---------------------------------------------------------------------------
167 167 # 'RichIPythonWidget' protected interface
168 168 #---------------------------------------------------------------------------
169 169
170 170 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
171 171 """ Append raw JPG data to the widget."""
172 172 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
173 173
174 174 def _append_png(self, png, before_prompt=False, metadata=None):
175 175 """ Append raw PNG data to the widget.
176 176 """
177 177 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
178 178
179 179 def _append_svg(self, svg, before_prompt=False):
180 180 """ Append raw SVG data to the widget.
181 181 """
182 182 self._append_custom(self._insert_svg, svg, before_prompt)
183 183
184 184 def _add_image(self, image):
185 185 """ Adds the specified QImage to the document and returns a
186 186 QTextImageFormat that references it.
187 187 """
188 188 document = self._control.document()
189 189 name = str(image.cacheKey())
190 190 document.addResource(QtGui.QTextDocument.ImageResource,
191 191 QtCore.QUrl(name), image)
192 192 format = QtGui.QTextImageFormat()
193 193 format.setName(name)
194 194 return format
195 195
196 196 def _copy_image(self, name):
197 197 """ Copies the ImageResource with 'name' to the clipboard.
198 198 """
199 199 image = self._get_image(name)
200 200 QtGui.QApplication.clipboard().setImage(image)
201 201
202 202 def _get_image(self, name):
203 203 """ Returns the QImage stored as the ImageResource with 'name'.
204 204 """
205 205 document = self._control.document()
206 206 image = document.resource(QtGui.QTextDocument.ImageResource,
207 207 QtCore.QUrl(name))
208 208 return image
209 209
210 210 def _get_image_tag(self, match, path = None, format = "png"):
211 211 """ Return (X)HTML mark-up for the image-tag given by match.
212 212
213 213 Parameters
214 214 ----------
215 215 match : re.SRE_Match
216 216 A match to an HTML image tag as exported by Qt, with
217 217 match.group("Name") containing the matched image ID.
218 218
219 219 path : string|None, optional [default None]
220 220 If not None, specifies a path to which supporting files may be
221 221 written (e.g., for linked images). If None, all images are to be
222 222 included inline.
223 223
224 224 format : "png"|"svg"|"jpg", optional [default "png"]
225 225 Format for returned or referenced images.
226 226 """
227 227 if format in ("png","jpg"):
228 228 try:
229 229 image = self._get_image(match.group("name"))
230 230 except KeyError:
231 231 return "<b>Couldn't find image %s</b>" % match.group("name")
232 232
233 233 if path is not None:
234 234 if not os.path.exists(path):
235 235 os.mkdir(path)
236 236 relpath = os.path.basename(path)
237 237 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
238 238 "PNG"):
239 239 return '<img src="%s/qt_img%s.%s">' % (relpath,
240 240 match.group("name"),format)
241 241 else:
242 242 return "<b>Couldn't save image!</b>"
243 243 else:
244 244 ba = QtCore.QByteArray()
245 245 buffer_ = QtCore.QBuffer(ba)
246 246 buffer_.open(QtCore.QIODevice.WriteOnly)
247 247 image.save(buffer_, format.upper())
248 248 buffer_.close()
249 249 return '<img src="data:image/%s;base64,\n%s\n" />' % (
250 250 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
251 251
252 252 elif format == "svg":
253 253 try:
254 254 svg = str(self._name_to_svg_map[match.group("name")])
255 255 except KeyError:
256 256 if not self._svg_warning_displayed:
257 257 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
258 258 'Cannot convert PNG images to SVG, export with PNG figures instead. '
259 259 'If you want to export matplotlib figures as SVG, add '
260 260 'to your ipython config:\n\n'
261 261 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
262 262 'And regenerate the figures.',
263 263 QtGui.QMessageBox.Ok)
264 264 self._svg_warning_displayed = True
265 265 return ("<b>Cannot convert PNG images to SVG.</b> "
266 266 "You must export this session with PNG images. "
267 267 "If you want to export matplotlib figures as SVG, add to your config "
268 268 "<span>c.InlineBackend.figure_format = 'svg'</span> "
269 269 "and regenerate the figures.")
270 270
271 271 # Not currently checking path, because it's tricky to find a
272 272 # cross-browser way to embed external SVG images (e.g., via
273 273 # object or embed tags).
274 274
275 275 # Chop stand-alone header from matplotlib SVG
276 276 offset = svg.find("<svg")
277 277 assert(offset > -1)
278 278
279 279 return svg[offset:]
280 280
281 281 else:
282 282 return '<b>Unrecognized image format</b>'
283 283
284 284 def _insert_jpg(self, cursor, jpg, metadata=None):
285 285 """ Insert raw PNG data into the widget."""
286 286 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
287 287
288 288 def _insert_png(self, cursor, png, metadata=None):
289 289 """ Insert raw PNG data into the widget.
290 290 """
291 291 self._insert_img(cursor, png, 'png', metadata=metadata)
292 292
293 293 def _insert_img(self, cursor, img, fmt, metadata=None):
294 294 """ insert a raw image, jpg or png """
295 295 if metadata:
296 296 width = metadata.get('width', None)
297 297 height = metadata.get('height', None)
298 298 else:
299 299 width = height = None
300 300 try:
301 301 image = QtGui.QImage()
302 302 image.loadFromData(img, fmt.upper())
303 303 if width and height:
304 304 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
305 305 elif width and not height:
306 306 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
307 307 elif height and not width:
308 308 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
309 309 except ValueError:
310 310 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
311 311 else:
312 312 format = self._add_image(image)
313 313 cursor.insertBlock()
314 314 cursor.insertImage(format)
315 315 cursor.insertBlock()
316 316
317 317 def _insert_svg(self, cursor, svg):
318 318 """ Insert raw SVG data into the widet.
319 319 """
320 320 try:
321 321 image = svg_to_image(svg)
322 322 except ValueError:
323 323 self._insert_plain_text(cursor, 'Received invalid SVG data.')
324 324 else:
325 325 format = self._add_image(image)
326 326 self._name_to_svg_map[format.name()] = svg
327 327 cursor.insertBlock()
328 328 cursor.insertImage(format)
329 329 cursor.insertBlock()
330 330
331 331 def _save_image(self, name, format='PNG'):
332 332 """ Shows a save dialog for the ImageResource with 'name'.
333 333 """
334 334 dialog = QtGui.QFileDialog(self._control, 'Save Image')
335 335 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
336 336 dialog.setDefaultSuffix(format.lower())
337 337 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
338 338 if dialog.exec_():
339 339 filename = dialog.selectedFiles()[0]
340 340 image = self._get_image(name)
341 341 image.save(filename, format)
@@ -1,220 +1,220 b''
1 1 """ Defines a KernelManager that provides signals and slots.
2 2 """
3 3
4 4 # System library imports.
5 5 from IPython.external.qt import QtCore
6 6
7 7 # IPython imports.
8 8 from IPython.utils.traitlets import HasTraits, Type
9 from util import MetaQObjectHasTraits, SuperQObject
9 from .util import MetaQObjectHasTraits, SuperQObject
10 10
11 11
12 12 class ChannelQObject(SuperQObject):
13 13
14 14 # Emitted when the channel is started.
15 15 started = QtCore.Signal()
16 16
17 17 # Emitted when the channel is stopped.
18 18 stopped = QtCore.Signal()
19 19
20 20 #---------------------------------------------------------------------------
21 21 # Channel interface
22 22 #---------------------------------------------------------------------------
23 23
24 24 def start(self):
25 25 """ Reimplemented to emit signal.
26 26 """
27 27 super(ChannelQObject, self).start()
28 28 self.started.emit()
29 29
30 30 def stop(self):
31 31 """ Reimplemented to emit signal.
32 32 """
33 33 super(ChannelQObject, self).stop()
34 34 self.stopped.emit()
35 35
36 36 #---------------------------------------------------------------------------
37 37 # InProcessChannel interface
38 38 #---------------------------------------------------------------------------
39 39
40 40 def call_handlers_later(self, *args, **kwds):
41 41 """ Call the message handlers later.
42 42 """
43 43 do_later = lambda: self.call_handlers(*args, **kwds)
44 44 QtCore.QTimer.singleShot(0, do_later)
45 45
46 46 def process_events(self):
47 47 """ Process any pending GUI events.
48 48 """
49 49 QtCore.QCoreApplication.instance().processEvents()
50 50
51 51
52 52 class QtShellChannelMixin(ChannelQObject):
53 53
54 54 # Emitted when any message is received.
55 55 message_received = QtCore.Signal(object)
56 56
57 57 # Emitted when a reply has been received for the corresponding request type.
58 58 execute_reply = QtCore.Signal(object)
59 59 complete_reply = QtCore.Signal(object)
60 60 object_info_reply = QtCore.Signal(object)
61 61 history_reply = QtCore.Signal(object)
62 62
63 63 #---------------------------------------------------------------------------
64 64 # 'ShellChannel' interface
65 65 #---------------------------------------------------------------------------
66 66
67 67 def call_handlers(self, msg):
68 68 """ Reimplemented to emit signals instead of making callbacks.
69 69 """
70 70 # Emit the generic signal.
71 71 self.message_received.emit(msg)
72 72
73 73 # Emit signals for specialized message types.
74 74 msg_type = msg['header']['msg_type']
75 75 signal = getattr(self, msg_type, None)
76 76 if signal:
77 77 signal.emit(msg)
78 78
79 79
80 80 class QtIOPubChannelMixin(ChannelQObject):
81 81
82 82 # Emitted when any message is received.
83 83 message_received = QtCore.Signal(object)
84 84
85 85 # Emitted when a message of type 'stream' is received.
86 86 stream_received = QtCore.Signal(object)
87 87
88 88 # Emitted when a message of type 'pyin' is received.
89 89 pyin_received = QtCore.Signal(object)
90 90
91 91 # Emitted when a message of type 'pyout' is received.
92 92 pyout_received = QtCore.Signal(object)
93 93
94 94 # Emitted when a message of type 'pyerr' is received.
95 95 pyerr_received = QtCore.Signal(object)
96 96
97 97 # Emitted when a message of type 'display_data' is received
98 98 display_data_received = QtCore.Signal(object)
99 99
100 100 # Emitted when a crash report message is received from the kernel's
101 101 # last-resort sys.excepthook.
102 102 crash_received = QtCore.Signal(object)
103 103
104 104 # Emitted when a shutdown is noticed.
105 105 shutdown_reply_received = QtCore.Signal(object)
106 106
107 107 #---------------------------------------------------------------------------
108 108 # 'IOPubChannel' interface
109 109 #---------------------------------------------------------------------------
110 110
111 111 def call_handlers(self, msg):
112 112 """ Reimplemented to emit signals instead of making callbacks.
113 113 """
114 114 # Emit the generic signal.
115 115 self.message_received.emit(msg)
116 116 # Emit signals for specialized message types.
117 117 msg_type = msg['header']['msg_type']
118 118 signal = getattr(self, msg_type + '_received', None)
119 119 if signal:
120 120 signal.emit(msg)
121 121 elif msg_type in ('stdout', 'stderr'):
122 122 self.stream_received.emit(msg)
123 123
124 124 def flush(self):
125 125 """ Reimplemented to ensure that signals are dispatched immediately.
126 126 """
127 127 super(QtIOPubChannelMixin, self).flush()
128 128 QtCore.QCoreApplication.instance().processEvents()
129 129
130 130
131 131 class QtStdInChannelMixin(ChannelQObject):
132 132
133 133 # Emitted when any message is received.
134 134 message_received = QtCore.Signal(object)
135 135
136 136 # Emitted when an input request is received.
137 137 input_requested = QtCore.Signal(object)
138 138
139 139 #---------------------------------------------------------------------------
140 140 # 'StdInChannel' interface
141 141 #---------------------------------------------------------------------------
142 142
143 143 def call_handlers(self, msg):
144 144 """ Reimplemented to emit signals instead of making callbacks.
145 145 """
146 146 # Emit the generic signal.
147 147 self.message_received.emit(msg)
148 148
149 149 # Emit signals for specialized message types.
150 150 msg_type = msg['header']['msg_type']
151 151 if msg_type == 'input_request':
152 152 self.input_requested.emit(msg)
153 153
154 154
155 155 class QtHBChannelMixin(ChannelQObject):
156 156
157 157 # Emitted when the kernel has died.
158 158 kernel_died = QtCore.Signal(object)
159 159
160 160 #---------------------------------------------------------------------------
161 161 # 'HBChannel' interface
162 162 #---------------------------------------------------------------------------
163 163
164 164 def call_handlers(self, since_last_heartbeat):
165 165 """ Reimplemented to emit signals instead of making callbacks.
166 166 """
167 167 # Emit the generic signal.
168 168 self.kernel_died.emit(since_last_heartbeat)
169 169
170 170
171 171 class QtKernelRestarterMixin(HasTraits, SuperQObject):
172 172
173 173 __metaclass__ = MetaQObjectHasTraits
174 174 _timer = None
175 175
176 176
177 177 class QtKernelManagerMixin(HasTraits, SuperQObject):
178 178 """ A KernelClient that provides signals and slots.
179 179 """
180 180
181 181 __metaclass__ = MetaQObjectHasTraits
182 182
183 183 kernel_restarted = QtCore.Signal()
184 184
185 185
186 186 class QtKernelClientMixin(HasTraits, SuperQObject):
187 187 """ A KernelClient that provides signals and slots.
188 188 """
189 189
190 190 __metaclass__ = MetaQObjectHasTraits
191 191
192 192 # Emitted when the kernel client has started listening.
193 193 started_channels = QtCore.Signal()
194 194
195 195 # Emitted when the kernel client has stopped listening.
196 196 stopped_channels = QtCore.Signal()
197 197
198 198 # Use Qt-specific channel classes that emit signals.
199 199 iopub_channel_class = Type(QtIOPubChannelMixin)
200 200 shell_channel_class = Type(QtShellChannelMixin)
201 201 stdin_channel_class = Type(QtStdInChannelMixin)
202 202 hb_channel_class = Type(QtHBChannelMixin)
203 203
204 204 #---------------------------------------------------------------------------
205 205 # 'KernelClient' interface
206 206 #---------------------------------------------------------------------------
207 207
208 208 #------ Channel management -------------------------------------------------
209 209
210 210 def start_channels(self, *args, **kw):
211 211 """ Reimplemented to emit signal.
212 212 """
213 213 super(QtKernelClientMixin, self).start_channels(*args, **kw)
214 214 self.started_channels.emit()
215 215
216 216 def stop_channels(self):
217 217 """ Reimplemented to emit signal.
218 218 """
219 219 super(QtKernelClientMixin, self).stop_channels()
220 220 self.stopped_channels.emit()
@@ -1,29 +1,29 b''
1 1 """Testing support (tools to test IPython itself).
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2009-2011 The IPython Development Team
6 6 #
7 7 # Distributed under the terms of the BSD License. The full license is in
8 8 # the file COPYING, distributed as part of this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Functions
13 13 #-----------------------------------------------------------------------------
14 14
15 15 # User-level entry point for testing
16 16 def test(all=False):
17 17 """Run the entire IPython test suite.
18 18
19 19 For fine-grained control, you should use the :file:`iptest` script supplied
20 20 with the IPython installation."""
21 21
22 22 # Do the import internally, so that this function doesn't increase total
23 23 # import time
24 from iptest import run_iptestall
24 from .iptest import run_iptestall
25 25 run_iptestall(inc_slow=all)
26 26
27 27 # So nose doesn't try to run this as a test itself and we end up with an
28 28 # infinite test loop
29 29 test.__test__ = False
@@ -1,381 +1,381 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Decorators for labeling test objects.
3 3
4 4 Decorators that merely return a modified version of the original function
5 5 object are straightforward. Decorators that return a new function object need
6 6 to use nose.tools.make_decorator(original_function)(decorator) in returning the
7 7 decorator, in order to preserve metadata such as function name, setup and
8 8 teardown functions and so on - see nose.tools for more information.
9 9
10 10 This module provides a set of useful decorators meant to be ready to use in
11 11 your own tests. See the bottom of the file for the ready-made ones, and if you
12 12 find yourself writing a new one that may be of generic use, add it here.
13 13
14 14 Included decorators:
15 15
16 16
17 17 Lightweight testing that remains unittest-compatible.
18 18
19 19 - An @as_unittest decorator can be used to tag any normal parameter-less
20 20 function as a unittest TestCase. Then, both nose and normal unittest will
21 21 recognize it as such. This will make it easier to migrate away from Nose if
22 22 we ever need/want to while maintaining very lightweight tests.
23 23
24 24 NOTE: This file contains IPython-specific decorators. Using the machinery in
25 25 IPython.external.decorators, we import either numpy.testing.decorators if numpy is
26 26 available, OR use equivalent code in IPython.external._decorators, which
27 27 we've copied verbatim from numpy.
28 28
29 29 Authors
30 30 -------
31 31
32 32 - Fernando Perez <Fernando.Perez@berkeley.edu>
33 33 """
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Copyright (C) 2009-2011 The IPython Development Team
37 37 #
38 38 # Distributed under the terms of the BSD License. The full license is in
39 39 # the file COPYING, distributed as part of this software.
40 40 #-----------------------------------------------------------------------------
41 41
42 42 #-----------------------------------------------------------------------------
43 43 # Imports
44 44 #-----------------------------------------------------------------------------
45 45
46 46 # Stdlib imports
47 47 import sys
48 48 import os
49 49 import tempfile
50 50 import unittest
51 51
52 52 # Third-party imports
53 53
54 54 # This is Michele Simionato's decorator module, kept verbatim.
55 55 from IPython.external.decorator import decorator
56 56
57 57 # Expose the unittest-driven decorators
58 from ipunittest import ipdoctest, ipdocstring
58 from .ipunittest import ipdoctest, ipdocstring
59 59
60 60 # Grab the numpy-specific decorators which we keep in a file that we
61 61 # occasionally update from upstream: decorators.py is a copy of
62 62 # numpy.testing.decorators, we expose all of it here.
63 63 from IPython.external.decorators import *
64 64
65 65 # For onlyif_cmd_exists decorator
66 66 from IPython.utils.process import is_cmd_found
67 67
68 68 #-----------------------------------------------------------------------------
69 69 # Classes and functions
70 70 #-----------------------------------------------------------------------------
71 71
72 72 # Simple example of the basic idea
73 73 def as_unittest(func):
74 74 """Decorator to make a simple function into a normal test via unittest."""
75 75 class Tester(unittest.TestCase):
76 76 def test(self):
77 77 func()
78 78
79 79 Tester.__name__ = func.__name__
80 80
81 81 return Tester
82 82
83 83 # Utility functions
84 84
85 85 def apply_wrapper(wrapper,func):
86 86 """Apply a wrapper to a function for decoration.
87 87
88 88 This mixes Michele Simionato's decorator tool with nose's make_decorator,
89 89 to apply a wrapper in a decorator so that all nose attributes, as well as
90 90 function signature and other properties, survive the decoration cleanly.
91 91 This will ensure that wrapped functions can still be well introspected via
92 92 IPython, for example.
93 93 """
94 94 import nose.tools
95 95
96 96 return decorator(wrapper,nose.tools.make_decorator(func)(wrapper))
97 97
98 98
99 99 def make_label_dec(label,ds=None):
100 100 """Factory function to create a decorator that applies one or more labels.
101 101
102 102 Parameters
103 103 ----------
104 104 label : string or sequence
105 105 One or more labels that will be applied by the decorator to the functions
106 106 it decorates. Labels are attributes of the decorated function with their
107 107 value set to True.
108 108
109 109 ds : string
110 110 An optional docstring for the resulting decorator. If not given, a
111 111 default docstring is auto-generated.
112 112
113 113 Returns
114 114 -------
115 115 A decorator.
116 116
117 117 Examples
118 118 --------
119 119
120 120 A simple labeling decorator:
121 121
122 122 >>> slow = make_label_dec('slow')
123 123 >>> slow.__doc__
124 124 "Labels a test as 'slow'."
125 125
126 126 And one that uses multiple labels and a custom docstring:
127 127
128 128 >>> rare = make_label_dec(['slow','hard'],
129 129 ... "Mix labels 'slow' and 'hard' for rare tests.")
130 130 >>> rare.__doc__
131 131 "Mix labels 'slow' and 'hard' for rare tests."
132 132
133 133 Now, let's test using this one:
134 134 >>> @rare
135 135 ... def f(): pass
136 136 ...
137 137 >>>
138 138 >>> f.slow
139 139 True
140 140 >>> f.hard
141 141 True
142 142 """
143 143
144 144 if isinstance(label,basestring):
145 145 labels = [label]
146 146 else:
147 147 labels = label
148 148
149 149 # Validate that the given label(s) are OK for use in setattr() by doing a
150 150 # dry run on a dummy function.
151 151 tmp = lambda : None
152 152 for label in labels:
153 153 setattr(tmp,label,True)
154 154
155 155 # This is the actual decorator we'll return
156 156 def decor(f):
157 157 for label in labels:
158 158 setattr(f,label,True)
159 159 return f
160 160
161 161 # Apply the user's docstring, or autogenerate a basic one
162 162 if ds is None:
163 163 ds = "Labels a test as %r." % label
164 164 decor.__doc__ = ds
165 165
166 166 return decor
167 167
168 168
169 169 # Inspired by numpy's skipif, but uses the full apply_wrapper utility to
170 170 # preserve function metadata better and allows the skip condition to be a
171 171 # callable.
172 172 def skipif(skip_condition, msg=None):
173 173 ''' Make function raise SkipTest exception if skip_condition is true
174 174
175 175 Parameters
176 176 ----------
177 177 skip_condition : bool or callable.
178 178 Flag to determine whether to skip test. If the condition is a
179 179 callable, it is used at runtime to dynamically make the decision. This
180 180 is useful for tests that may require costly imports, to delay the cost
181 181 until the test suite is actually executed.
182 182 msg : string
183 183 Message to give on raising a SkipTest exception
184 184
185 185 Returns
186 186 -------
187 187 decorator : function
188 188 Decorator, which, when applied to a function, causes SkipTest
189 189 to be raised when the skip_condition was True, and the function
190 190 to be called normally otherwise.
191 191
192 192 Notes
193 193 -----
194 194 You will see from the code that we had to further decorate the
195 195 decorator with the nose.tools.make_decorator function in order to
196 196 transmit function name, and various other metadata.
197 197 '''
198 198
199 199 def skip_decorator(f):
200 200 # Local import to avoid a hard nose dependency and only incur the
201 201 # import time overhead at actual test-time.
202 202 import nose
203 203
204 204 # Allow for both boolean or callable skip conditions.
205 205 if callable(skip_condition):
206 206 skip_val = skip_condition
207 207 else:
208 208 skip_val = lambda : skip_condition
209 209
210 210 def get_msg(func,msg=None):
211 211 """Skip message with information about function being skipped."""
212 212 if msg is None: out = 'Test skipped due to test condition.'
213 213 else: out = msg
214 214 return "Skipping test: %s. %s" % (func.__name__,out)
215 215
216 216 # We need to define *two* skippers because Python doesn't allow both
217 217 # return with value and yield inside the same function.
218 218 def skipper_func(*args, **kwargs):
219 219 """Skipper for normal test functions."""
220 220 if skip_val():
221 221 raise nose.SkipTest(get_msg(f,msg))
222 222 else:
223 223 return f(*args, **kwargs)
224 224
225 225 def skipper_gen(*args, **kwargs):
226 226 """Skipper for test generators."""
227 227 if skip_val():
228 228 raise nose.SkipTest(get_msg(f,msg))
229 229 else:
230 230 for x in f(*args, **kwargs):
231 231 yield x
232 232
233 233 # Choose the right skipper to use when building the actual generator.
234 234 if nose.util.isgenerator(f):
235 235 skipper = skipper_gen
236 236 else:
237 237 skipper = skipper_func
238 238
239 239 return nose.tools.make_decorator(f)(skipper)
240 240
241 241 return skip_decorator
242 242
243 243 # A version with the condition set to true, common case just to attach a message
244 244 # to a skip decorator
245 245 def skip(msg=None):
246 246 """Decorator factory - mark a test function for skipping from test suite.
247 247
248 248 Parameters
249 249 ----------
250 250 msg : string
251 251 Optional message to be added.
252 252
253 253 Returns
254 254 -------
255 255 decorator : function
256 256 Decorator, which, when applied to a function, causes SkipTest
257 257 to be raised, with the optional message added.
258 258 """
259 259
260 260 return skipif(True,msg)
261 261
262 262
263 263 def onlyif(condition, msg):
264 264 """The reverse from skipif, see skipif for details."""
265 265
266 266 if callable(condition):
267 267 skip_condition = lambda : not condition()
268 268 else:
269 269 skip_condition = lambda : not condition
270 270
271 271 return skipif(skip_condition, msg)
272 272
273 273 #-----------------------------------------------------------------------------
274 274 # Utility functions for decorators
275 275 def module_not_available(module):
276 276 """Can module be imported? Returns true if module does NOT import.
277 277
278 278 This is used to make a decorator to skip tests that require module to be
279 279 available, but delay the 'import numpy' to test execution time.
280 280 """
281 281 try:
282 282 mod = __import__(module)
283 283 mod_not_avail = False
284 284 except ImportError:
285 285 mod_not_avail = True
286 286
287 287 return mod_not_avail
288 288
289 289
290 290 def decorated_dummy(dec, name):
291 291 """Return a dummy function decorated with dec, with the given name.
292 292
293 293 Examples
294 294 --------
295 295 import IPython.testing.decorators as dec
296 296 setup = dec.decorated_dummy(dec.skip_if_no_x11, __name__)
297 297 """
298 298 dummy = lambda: None
299 299 dummy.__name__ = name
300 300 return dec(dummy)
301 301
302 302 #-----------------------------------------------------------------------------
303 303 # Decorators for public use
304 304
305 305 # Decorators to skip certain tests on specific platforms.
306 306 skip_win32 = skipif(sys.platform == 'win32',
307 307 "This test does not run under Windows")
308 308 skip_linux = skipif(sys.platform.startswith('linux'),
309 309 "This test does not run under Linux")
310 310 skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X")
311 311
312 312
313 313 # Decorators to skip tests if not on specific platforms.
314 314 skip_if_not_win32 = skipif(sys.platform != 'win32',
315 315 "This test only runs under Windows")
316 316 skip_if_not_linux = skipif(not sys.platform.startswith('linux'),
317 317 "This test only runs under Linux")
318 318 skip_if_not_osx = skipif(sys.platform != 'darwin',
319 319 "This test only runs under OSX")
320 320
321 321
322 322 _x11_skip_cond = (sys.platform not in ('darwin', 'win32') and
323 323 os.environ.get('DISPLAY', '') == '')
324 324 _x11_skip_msg = "Skipped under *nix when X11/XOrg not available"
325 325
326 326 skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg)
327 327
328 328 # not a decorator itself, returns a dummy function to be used as setup
329 329 def skip_file_no_x11(name):
330 330 return decorated_dummy(skip_if_no_x11, name) if _x11_skip_cond else None
331 331
332 332 # Other skip decorators
333 333
334 334 # generic skip without module
335 335 skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod)
336 336
337 337 skipif_not_numpy = skip_without('numpy')
338 338
339 339 skipif_not_matplotlib = skip_without('matplotlib')
340 340
341 341 skipif_not_sympy = skip_without('sympy')
342 342
343 343 skip_known_failure = knownfailureif(True,'This test is known to fail')
344 344
345 345 known_failure_py3 = knownfailureif(sys.version_info[0] >= 3,
346 346 'This test is known to fail on Python 3.')
347 347
348 348 # A null 'decorator', useful to make more readable code that needs to pick
349 349 # between different decorators based on OS or other conditions
350 350 null_deco = lambda f: f
351 351
352 352 # Some tests only run where we can use unicode paths. Note that we can't just
353 353 # check os.path.supports_unicode_filenames, which is always False on Linux.
354 354 try:
355 355 f = tempfile.NamedTemporaryFile(prefix=u"tmp€")
356 356 except UnicodeEncodeError:
357 357 unicode_paths = False
358 358 else:
359 359 unicode_paths = True
360 360 f.close()
361 361
362 362 onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable "
363 363 "where we can use unicode in filenames."))
364 364
365 365
366 366 def onlyif_cmds_exist(*commands):
367 367 """
368 368 Decorator to skip test when at least one of `commands` is not found.
369 369 """
370 370 for cmd in commands:
371 371 try:
372 372 if not is_cmd_found(cmd):
373 373 return skip("This test runs only if command '{0}' "
374 374 "is installed".format(cmd))
375 375 except ImportError as e:
376 376 # is_cmd_found uses pywin32 on windows, which might not be available
377 377 if sys.platform == 'win32' and 'pywin32' in str(e):
378 378 return skip("This test runs only if pywin32 and command '{0}' "
379 379 "is installed".format(cmd))
380 380 raise e
381 381 return null_deco
@@ -1,18 +1,18 b''
1 1 #!/usr/bin/env python
2 2 """Nose-based test runner.
3 3 """
4 4
5 5 from nose.core import main
6 6 from nose.plugins.builtin import plugins
7 7 from nose.plugins.doctests import Doctest
8 8
9 import ipdoctest
10 from ipdoctest import IPDocTestRunner
9 from . import ipdoctest
10 from .ipdoctest import IPDocTestRunner
11 11
12 12 if __name__ == '__main__':
13 13 print 'WARNING: this code is incomplete!'
14 14 print
15 15
16 16 pp = [x() for x in plugins] # activate all builtin plugins first
17 17 main(testRunner=IPDocTestRunner(),
18 18 plugins=pp+[ipdoctest.IPythonDoctest(),Doctest()])
@@ -1,351 +1,351 b''
1 1 # encoding: utf-8
2 2
3 3 """Pickle related utilities. Perhaps this should be called 'can'."""
4 4
5 5 __docformat__ = "restructuredtext en"
6 6
7 7 #-------------------------------------------------------------------------------
8 8 # Copyright (C) 2008-2011 The IPython Development Team
9 9 #
10 10 # Distributed under the terms of the BSD License. The full license is in
11 11 # the file COPYING, distributed as part of this software.
12 12 #-------------------------------------------------------------------------------
13 13
14 14 #-------------------------------------------------------------------------------
15 15 # Imports
16 16 #-------------------------------------------------------------------------------
17 17
18 18 import copy
19 19 import logging
20 20 import sys
21 21 from types import FunctionType
22 22
23 23 try:
24 24 import cPickle as pickle
25 25 except ImportError:
26 26 import pickle
27 27
28 import codeutil # This registers a hook when it's imported
29 import py3compat
30 from importstring import import_item
28 from . import codeutil # This registers a hook when it's imported
29 from . import py3compat
30 from .importstring import import_item
31 31
32 32 from IPython.config import Application
33 33
34 34 if py3compat.PY3:
35 35 buffer = memoryview
36 36 class_type = type
37 37 else:
38 38 from types import ClassType
39 39 class_type = (type, ClassType)
40 40
41 41 #-------------------------------------------------------------------------------
42 42 # Classes
43 43 #-------------------------------------------------------------------------------
44 44
45 45
46 46 class CannedObject(object):
47 47 def __init__(self, obj, keys=[], hook=None):
48 48 """can an object for safe pickling
49 49
50 50 Parameters
51 51 ==========
52 52
53 53 obj:
54 54 The object to be canned
55 55 keys: list (optional)
56 56 list of attribute names that will be explicitly canned / uncanned
57 57 hook: callable (optional)
58 58 An optional extra callable,
59 59 which can do additional processing of the uncanned object.
60 60
61 61 large data may be offloaded into the buffers list,
62 62 used for zero-copy transfers.
63 63 """
64 64 self.keys = keys
65 65 self.obj = copy.copy(obj)
66 66 self.hook = can(hook)
67 67 for key in keys:
68 68 setattr(self.obj, key, can(getattr(obj, key)))
69 69
70 70 self.buffers = []
71 71
72 72 def get_object(self, g=None):
73 73 if g is None:
74 74 g = {}
75 75 obj = self.obj
76 76 for key in self.keys:
77 77 setattr(obj, key, uncan(getattr(obj, key), g))
78 78
79 79 if self.hook:
80 80 self.hook = uncan(self.hook, g)
81 81 self.hook(obj, g)
82 82 return self.obj
83 83
84 84
85 85 class Reference(CannedObject):
86 86 """object for wrapping a remote reference by name."""
87 87 def __init__(self, name):
88 88 if not isinstance(name, basestring):
89 89 raise TypeError("illegal name: %r"%name)
90 90 self.name = name
91 91 self.buffers = []
92 92
93 93 def __repr__(self):
94 94 return "<Reference: %r>"%self.name
95 95
96 96 def get_object(self, g=None):
97 97 if g is None:
98 98 g = {}
99 99
100 100 return eval(self.name, g)
101 101
102 102
103 103 class CannedFunction(CannedObject):
104 104
105 105 def __init__(self, f):
106 106 self._check_type(f)
107 107 self.code = f.func_code
108 108 if f.func_defaults:
109 109 self.defaults = [ can(fd) for fd in f.func_defaults ]
110 110 else:
111 111 self.defaults = None
112 112 self.module = f.__module__ or '__main__'
113 113 self.__name__ = f.__name__
114 114 self.buffers = []
115 115
116 116 def _check_type(self, obj):
117 117 assert isinstance(obj, FunctionType), "Not a function type"
118 118
119 119 def get_object(self, g=None):
120 120 # try to load function back into its module:
121 121 if not self.module.startswith('__'):
122 122 __import__(self.module)
123 123 g = sys.modules[self.module].__dict__
124 124
125 125 if g is None:
126 126 g = {}
127 127 if self.defaults:
128 128 defaults = tuple(uncan(cfd, g) for cfd in self.defaults)
129 129 else:
130 130 defaults = None
131 131 newFunc = FunctionType(self.code, g, self.__name__, defaults)
132 132 return newFunc
133 133
134 134 class CannedClass(CannedObject):
135 135
136 136 def __init__(self, cls):
137 137 self._check_type(cls)
138 138 self.name = cls.__name__
139 139 self.old_style = not isinstance(cls, type)
140 140 self._canned_dict = {}
141 141 for k,v in cls.__dict__.items():
142 142 if k not in ('__weakref__', '__dict__'):
143 143 self._canned_dict[k] = can(v)
144 144 if self.old_style:
145 145 mro = []
146 146 else:
147 147 mro = cls.mro()
148 148
149 149 self.parents = [ can(c) for c in mro[1:] ]
150 150 self.buffers = []
151 151
152 152 def _check_type(self, obj):
153 153 assert isinstance(obj, class_type), "Not a class type"
154 154
155 155 def get_object(self, g=None):
156 156 parents = tuple(uncan(p, g) for p in self.parents)
157 157 return type(self.name, parents, uncan_dict(self._canned_dict, g=g))
158 158
159 159 class CannedArray(CannedObject):
160 160 def __init__(self, obj):
161 161 from numpy import ascontiguousarray
162 162 self.shape = obj.shape
163 163 self.dtype = obj.dtype.descr if obj.dtype.fields else obj.dtype.str
164 164 if sum(obj.shape) == 0:
165 165 # just pickle it
166 166 self.buffers = [pickle.dumps(obj, -1)]
167 167 else:
168 168 # ensure contiguous
169 169 obj = ascontiguousarray(obj, dtype=None)
170 170 self.buffers = [buffer(obj)]
171 171
172 172 def get_object(self, g=None):
173 173 from numpy import frombuffer
174 174 data = self.buffers[0]
175 175 if sum(self.shape) == 0:
176 176 # no shape, we just pickled it
177 177 return pickle.loads(data)
178 178 else:
179 179 return frombuffer(data, dtype=self.dtype).reshape(self.shape)
180 180
181 181
182 182 class CannedBytes(CannedObject):
183 183 wrap = bytes
184 184 def __init__(self, obj):
185 185 self.buffers = [obj]
186 186
187 187 def get_object(self, g=None):
188 188 data = self.buffers[0]
189 189 return self.wrap(data)
190 190
191 191 def CannedBuffer(CannedBytes):
192 192 wrap = buffer
193 193
194 194 #-------------------------------------------------------------------------------
195 195 # Functions
196 196 #-------------------------------------------------------------------------------
197 197
198 198 def _logger():
199 199 """get the logger for the current Application
200 200
201 201 the root logger will be used if no Application is running
202 202 """
203 203 if Application.initialized():
204 204 logger = Application.instance().log
205 205 else:
206 206 logger = logging.getLogger()
207 207 if not logger.handlers:
208 208 logging.basicConfig()
209 209
210 210 return logger
211 211
212 212 def _import_mapping(mapping, original=None):
213 213 """import any string-keys in a type mapping
214 214
215 215 """
216 216 log = _logger()
217 217 log.debug("Importing canning map")
218 218 for key,value in mapping.items():
219 219 if isinstance(key, basestring):
220 220 try:
221 221 cls = import_item(key)
222 222 except Exception:
223 223 if original and key not in original:
224 224 # only message on user-added classes
225 225 log.error("canning class not importable: %r", key, exc_info=True)
226 226 mapping.pop(key)
227 227 else:
228 228 mapping[cls] = mapping.pop(key)
229 229
230 230 def istype(obj, check):
231 231 """like isinstance(obj, check), but strict
232 232
233 233 This won't catch subclasses.
234 234 """
235 235 if isinstance(check, tuple):
236 236 for cls in check:
237 237 if type(obj) is cls:
238 238 return True
239 239 return False
240 240 else:
241 241 return type(obj) is check
242 242
243 243 def can(obj):
244 244 """prepare an object for pickling"""
245 245
246 246 import_needed = False
247 247
248 248 for cls,canner in can_map.iteritems():
249 249 if isinstance(cls, basestring):
250 250 import_needed = True
251 251 break
252 252 elif istype(obj, cls):
253 253 return canner(obj)
254 254
255 255 if import_needed:
256 256 # perform can_map imports, then try again
257 257 # this will usually only happen once
258 258 _import_mapping(can_map, _original_can_map)
259 259 return can(obj)
260 260
261 261 return obj
262 262
263 263 def can_class(obj):
264 264 if isinstance(obj, class_type) and obj.__module__ == '__main__':
265 265 return CannedClass(obj)
266 266 else:
267 267 return obj
268 268
269 269 def can_dict(obj):
270 270 """can the *values* of a dict"""
271 271 if istype(obj, dict):
272 272 newobj = {}
273 273 for k, v in obj.iteritems():
274 274 newobj[k] = can(v)
275 275 return newobj
276 276 else:
277 277 return obj
278 278
279 279 sequence_types = (list, tuple, set)
280 280
281 281 def can_sequence(obj):
282 282 """can the elements of a sequence"""
283 283 if istype(obj, sequence_types):
284 284 t = type(obj)
285 285 return t([can(i) for i in obj])
286 286 else:
287 287 return obj
288 288
289 289 def uncan(obj, g=None):
290 290 """invert canning"""
291 291
292 292 import_needed = False
293 293 for cls,uncanner in uncan_map.iteritems():
294 294 if isinstance(cls, basestring):
295 295 import_needed = True
296 296 break
297 297 elif isinstance(obj, cls):
298 298 return uncanner(obj, g)
299 299
300 300 if import_needed:
301 301 # perform uncan_map imports, then try again
302 302 # this will usually only happen once
303 303 _import_mapping(uncan_map, _original_uncan_map)
304 304 return uncan(obj, g)
305 305
306 306 return obj
307 307
308 308 def uncan_dict(obj, g=None):
309 309 if istype(obj, dict):
310 310 newobj = {}
311 311 for k, v in obj.iteritems():
312 312 newobj[k] = uncan(v,g)
313 313 return newobj
314 314 else:
315 315 return obj
316 316
317 317 def uncan_sequence(obj, g=None):
318 318 if istype(obj, sequence_types):
319 319 t = type(obj)
320 320 return t([uncan(i,g) for i in obj])
321 321 else:
322 322 return obj
323 323
324 324 def _uncan_dependent_hook(dep, g=None):
325 325 dep.check_dependency()
326 326
327 327 def can_dependent(obj):
328 328 return CannedObject(obj, keys=('f', 'df'), hook=_uncan_dependent_hook)
329 329
330 330 #-------------------------------------------------------------------------------
331 331 # API dictionaries
332 332 #-------------------------------------------------------------------------------
333 333
334 334 # These dicts can be extended for custom serialization of new objects
335 335
336 336 can_map = {
337 337 'IPython.parallel.dependent' : can_dependent,
338 338 'numpy.ndarray' : CannedArray,
339 339 FunctionType : CannedFunction,
340 340 bytes : CannedBytes,
341 341 buffer : CannedBuffer,
342 342 class_type : can_class,
343 343 }
344 344
345 345 uncan_map = {
346 346 CannedObject : lambda obj, g: obj.get_object(g),
347 347 }
348 348
349 349 # for use in _import_mapping:
350 350 _original_can_map = can_map.copy()
351 351 _original_uncan_map = uncan_map.copy()
@@ -1,9 +1,9 b''
1 1 """Load our patched versions of tokenize.
2 2 """
3 3
4 4 import sys
5 5
6 6 if sys.version_info[0] >= 3:
7 from _tokenize_py3 import *
7 from ._tokenize_py3 import *
8 8 else:
9 from _tokenize_py2 import *
9 from ._tokenize_py2 import *
General Comments 0
You need to be logged in to leave comments. Login now