##// END OF EJS Templates
Use explicit relative imports...
Thomas Kluyver -
Show More
@@ -1,4 +1,4 b''
1 try:
1 try:
2 from decorator import *
2 from decorator import *
3 except ImportError:
3 except ImportError:
4 from _decorator import *
4 from ._decorator import *
@@ -1,9 +1,9 b''
1 try:
1 try:
2 from numpy.testing.decorators import *
2 from numpy.testing.decorators import *
3 from numpy.testing.noseclasses import KnownFailure
3 from numpy.testing.noseclasses import KnownFailure
4 except ImportError:
4 except ImportError:
5 from _decorators import *
5 from ._decorators import *
6 try:
6 try:
7 from _numpy_testing_noseclasses import KnownFailure
7 from ._numpy_testing_noseclasses import KnownFailure
8 except ImportError:
8 except ImportError:
9 pass
9 pass
@@ -1,281 +1,281 b''
1 """
1 """
2 Decorators for labeling and modifying behavior of test objects.
2 Decorators for labeling and modifying behavior of test objects.
3
3
4 Decorators that merely return a modified version of the original
4 Decorators that merely return a modified version of the original
5 function object are straightforward. Decorators that return a new
5 function object are straightforward. Decorators that return a new
6 function object need to use
6 function object need to use
7 ::
7 ::
8
8
9 nose.tools.make_decorator(original_function)(decorator)
9 nose.tools.make_decorator(original_function)(decorator)
10
10
11 in returning the decorator, in order to preserve meta-data such as
11 in returning the decorator, in order to preserve meta-data such as
12 function name, setup and teardown functions and so on - see
12 function name, setup and teardown functions and so on - see
13 ``nose.tools`` for more information.
13 ``nose.tools`` for more information.
14
14
15 """
15 """
16 import warnings
16 import warnings
17
17
18 # IPython changes: make this work if numpy not available
18 # IPython changes: make this work if numpy not available
19 # Original code:
19 # Original code:
20 #from numpy.testing.utils import \
20 #from numpy.testing.utils import \
21 # WarningManager, WarningMessage
21 # WarningManager, WarningMessage
22 # Our version:
22 # Our version:
23 from _numpy_testing_utils import WarningManager
23 from ._numpy_testing_utils import WarningManager
24 try:
24 try:
25 from _numpy_testing_noseclasses import KnownFailureTest
25 from ._numpy_testing_noseclasses import KnownFailureTest
26 except:
26 except:
27 pass
27 pass
28
28
29 # End IPython changes
29 # End IPython changes
30
30
31 def slow(t):
31 def slow(t):
32 """
32 """
33 Label a test as 'slow'.
33 Label a test as 'slow'.
34
34
35 The exact definition of a slow test is obviously both subjective and
35 The exact definition of a slow test is obviously both subjective and
36 hardware-dependent, but in general any individual test that requires more
36 hardware-dependent, but in general any individual test that requires more
37 than a second or two should be labeled as slow (the whole suite consists of
37 than a second or two should be labeled as slow (the whole suite consists of
38 thousands of tests, so even a second is significant).
38 thousands of tests, so even a second is significant).
39
39
40 Parameters
40 Parameters
41 ----------
41 ----------
42 t : callable
42 t : callable
43 The test to label as slow.
43 The test to label as slow.
44
44
45 Returns
45 Returns
46 -------
46 -------
47 t : callable
47 t : callable
48 The decorated test `t`.
48 The decorated test `t`.
49
49
50 Examples
50 Examples
51 --------
51 --------
52 The `numpy.testing` module includes ``import decorators as dec``.
52 The `numpy.testing` module includes ``import decorators as dec``.
53 A test can be decorated as slow like this::
53 A test can be decorated as slow like this::
54
54
55 from numpy.testing import *
55 from numpy.testing import *
56
56
57 @dec.slow
57 @dec.slow
58 def test_big(self):
58 def test_big(self):
59 print 'Big, slow test'
59 print 'Big, slow test'
60
60
61 """
61 """
62
62
63 t.slow = True
63 t.slow = True
64 return t
64 return t
65
65
66 def setastest(tf=True):
66 def setastest(tf=True):
67 """
67 """
68 Signals to nose that this function is or is not a test.
68 Signals to nose that this function is or is not a test.
69
69
70 Parameters
70 Parameters
71 ----------
71 ----------
72 tf : bool
72 tf : bool
73 If True, specifies that the decorated callable is a test.
73 If True, specifies that the decorated callable is a test.
74 If False, specifies that the decorated callable is not a test.
74 If False, specifies that the decorated callable is not a test.
75 Default is True.
75 Default is True.
76
76
77 Notes
77 Notes
78 -----
78 -----
79 This decorator can't use the nose namespace, because it can be
79 This decorator can't use the nose namespace, because it can be
80 called from a non-test module. See also ``istest`` and ``nottest`` in
80 called from a non-test module. See also ``istest`` and ``nottest`` in
81 ``nose.tools``.
81 ``nose.tools``.
82
82
83 Examples
83 Examples
84 --------
84 --------
85 `setastest` can be used in the following way::
85 `setastest` can be used in the following way::
86
86
87 from numpy.testing.decorators import setastest
87 from numpy.testing.decorators import setastest
88
88
89 @setastest(False)
89 @setastest(False)
90 def func_with_test_in_name(arg1, arg2):
90 def func_with_test_in_name(arg1, arg2):
91 pass
91 pass
92
92
93 """
93 """
94 def set_test(t):
94 def set_test(t):
95 t.__test__ = tf
95 t.__test__ = tf
96 return t
96 return t
97 return set_test
97 return set_test
98
98
99 def skipif(skip_condition, msg=None):
99 def skipif(skip_condition, msg=None):
100 """
100 """
101 Make function raise SkipTest exception if a given condition is true.
101 Make function raise SkipTest exception if a given condition is true.
102
102
103 If the condition is a callable, it is used at runtime to dynamically
103 If the condition is a callable, it is used at runtime to dynamically
104 make the decision. This is useful for tests that may require costly
104 make the decision. This is useful for tests that may require costly
105 imports, to delay the cost until the test suite is actually executed.
105 imports, to delay the cost until the test suite is actually executed.
106
106
107 Parameters
107 Parameters
108 ----------
108 ----------
109 skip_condition : bool or callable
109 skip_condition : bool or callable
110 Flag to determine whether to skip the decorated test.
110 Flag to determine whether to skip the decorated test.
111 msg : str, optional
111 msg : str, optional
112 Message to give on raising a SkipTest exception. Default is None.
112 Message to give on raising a SkipTest exception. Default is None.
113
113
114 Returns
114 Returns
115 -------
115 -------
116 decorator : function
116 decorator : function
117 Decorator which, when applied to a function, causes SkipTest
117 Decorator which, when applied to a function, causes SkipTest
118 to be raised when `skip_condition` is True, and the function
118 to be raised when `skip_condition` is True, and the function
119 to be called normally otherwise.
119 to be called normally otherwise.
120
120
121 Notes
121 Notes
122 -----
122 -----
123 The decorator itself is decorated with the ``nose.tools.make_decorator``
123 The decorator itself is decorated with the ``nose.tools.make_decorator``
124 function in order to transmit function name, and various other metadata.
124 function in order to transmit function name, and various other metadata.
125
125
126 """
126 """
127
127
128 def skip_decorator(f):
128 def skip_decorator(f):
129 # Local import to avoid a hard nose dependency and only incur the
129 # Local import to avoid a hard nose dependency and only incur the
130 # import time overhead at actual test-time.
130 # import time overhead at actual test-time.
131 import nose
131 import nose
132
132
133 # Allow for both boolean or callable skip conditions.
133 # Allow for both boolean or callable skip conditions.
134 if callable(skip_condition):
134 if callable(skip_condition):
135 skip_val = lambda : skip_condition()
135 skip_val = lambda : skip_condition()
136 else:
136 else:
137 skip_val = lambda : skip_condition
137 skip_val = lambda : skip_condition
138
138
139 def get_msg(func,msg=None):
139 def get_msg(func,msg=None):
140 """Skip message with information about function being skipped."""
140 """Skip message with information about function being skipped."""
141 if msg is None:
141 if msg is None:
142 out = 'Test skipped due to test condition'
142 out = 'Test skipped due to test condition'
143 else:
143 else:
144 out = '\n'+msg
144 out = '\n'+msg
145
145
146 return "Skipping test: %s%s" % (func.__name__,out)
146 return "Skipping test: %s%s" % (func.__name__,out)
147
147
148 # We need to define *two* skippers because Python doesn't allow both
148 # We need to define *two* skippers because Python doesn't allow both
149 # return with value and yield inside the same function.
149 # return with value and yield inside the same function.
150 def skipper_func(*args, **kwargs):
150 def skipper_func(*args, **kwargs):
151 """Skipper for normal test functions."""
151 """Skipper for normal test functions."""
152 if skip_val():
152 if skip_val():
153 raise nose.SkipTest(get_msg(f,msg))
153 raise nose.SkipTest(get_msg(f,msg))
154 else:
154 else:
155 return f(*args, **kwargs)
155 return f(*args, **kwargs)
156
156
157 def skipper_gen(*args, **kwargs):
157 def skipper_gen(*args, **kwargs):
158 """Skipper for test generators."""
158 """Skipper for test generators."""
159 if skip_val():
159 if skip_val():
160 raise nose.SkipTest(get_msg(f,msg))
160 raise nose.SkipTest(get_msg(f,msg))
161 else:
161 else:
162 for x in f(*args, **kwargs):
162 for x in f(*args, **kwargs):
163 yield x
163 yield x
164
164
165 # Choose the right skipper to use when building the actual decorator.
165 # Choose the right skipper to use when building the actual decorator.
166 if nose.util.isgenerator(f):
166 if nose.util.isgenerator(f):
167 skipper = skipper_gen
167 skipper = skipper_gen
168 else:
168 else:
169 skipper = skipper_func
169 skipper = skipper_func
170
170
171 return nose.tools.make_decorator(f)(skipper)
171 return nose.tools.make_decorator(f)(skipper)
172
172
173 return skip_decorator
173 return skip_decorator
174
174
175 def knownfailureif(fail_condition, msg=None):
175 def knownfailureif(fail_condition, msg=None):
176 """
176 """
177 Make function raise KnownFailureTest exception if given condition is true.
177 Make function raise KnownFailureTest exception if given condition is true.
178
178
179 If the condition is a callable, it is used at runtime to dynamically
179 If the condition is a callable, it is used at runtime to dynamically
180 make the decision. This is useful for tests that may require costly
180 make the decision. This is useful for tests that may require costly
181 imports, to delay the cost until the test suite is actually executed.
181 imports, to delay the cost until the test suite is actually executed.
182
182
183 Parameters
183 Parameters
184 ----------
184 ----------
185 fail_condition : bool or callable
185 fail_condition : bool or callable
186 Flag to determine whether to mark the decorated test as a known
186 Flag to determine whether to mark the decorated test as a known
187 failure (if True) or not (if False).
187 failure (if True) or not (if False).
188 msg : str, optional
188 msg : str, optional
189 Message to give on raising a KnownFailureTest exception.
189 Message to give on raising a KnownFailureTest exception.
190 Default is None.
190 Default is None.
191
191
192 Returns
192 Returns
193 -------
193 -------
194 decorator : function
194 decorator : function
195 Decorator, which, when applied to a function, causes SkipTest
195 Decorator, which, when applied to a function, causes SkipTest
196 to be raised when `skip_condition` is True, and the function
196 to be raised when `skip_condition` is True, and the function
197 to be called normally otherwise.
197 to be called normally otherwise.
198
198
199 Notes
199 Notes
200 -----
200 -----
201 The decorator itself is decorated with the ``nose.tools.make_decorator``
201 The decorator itself is decorated with the ``nose.tools.make_decorator``
202 function in order to transmit function name, and various other metadata.
202 function in order to transmit function name, and various other metadata.
203
203
204 """
204 """
205 if msg is None:
205 if msg is None:
206 msg = 'Test skipped due to known failure'
206 msg = 'Test skipped due to known failure'
207
207
208 # Allow for both boolean or callable known failure conditions.
208 # Allow for both boolean or callable known failure conditions.
209 if callable(fail_condition):
209 if callable(fail_condition):
210 fail_val = lambda : fail_condition()
210 fail_val = lambda : fail_condition()
211 else:
211 else:
212 fail_val = lambda : fail_condition
212 fail_val = lambda : fail_condition
213
213
214 def knownfail_decorator(f):
214 def knownfail_decorator(f):
215 # Local import to avoid a hard nose dependency and only incur the
215 # Local import to avoid a hard nose dependency and only incur the
216 # import time overhead at actual test-time.
216 # import time overhead at actual test-time.
217 import nose
217 import nose
218 def knownfailer(*args, **kwargs):
218 def knownfailer(*args, **kwargs):
219 if fail_val():
219 if fail_val():
220 raise KnownFailureTest(msg)
220 raise KnownFailureTest(msg)
221 else:
221 else:
222 return f(*args, **kwargs)
222 return f(*args, **kwargs)
223 return nose.tools.make_decorator(f)(knownfailer)
223 return nose.tools.make_decorator(f)(knownfailer)
224
224
225 return knownfail_decorator
225 return knownfail_decorator
226
226
227 def deprecated(conditional=True):
227 def deprecated(conditional=True):
228 """
228 """
229 Filter deprecation warnings while running the test suite.
229 Filter deprecation warnings while running the test suite.
230
230
231 This decorator can be used to filter DeprecationWarning's, to avoid
231 This decorator can be used to filter DeprecationWarning's, to avoid
232 printing them during the test suite run, while checking that the test
232 printing them during the test suite run, while checking that the test
233 actually raises a DeprecationWarning.
233 actually raises a DeprecationWarning.
234
234
235 Parameters
235 Parameters
236 ----------
236 ----------
237 conditional : bool or callable, optional
237 conditional : bool or callable, optional
238 Flag to determine whether to mark test as deprecated or not. If the
238 Flag to determine whether to mark test as deprecated or not. If the
239 condition is a callable, it is used at runtime to dynamically make the
239 condition is a callable, it is used at runtime to dynamically make the
240 decision. Default is True.
240 decision. Default is True.
241
241
242 Returns
242 Returns
243 -------
243 -------
244 decorator : function
244 decorator : function
245 The `deprecated` decorator itself.
245 The `deprecated` decorator itself.
246
246
247 Notes
247 Notes
248 -----
248 -----
249 .. versionadded:: 1.4.0
249 .. versionadded:: 1.4.0
250
250
251 """
251 """
252 def deprecate_decorator(f):
252 def deprecate_decorator(f):
253 # Local import to avoid a hard nose dependency and only incur the
253 # Local import to avoid a hard nose dependency and only incur the
254 # import time overhead at actual test-time.
254 # import time overhead at actual test-time.
255 import nose
255 import nose
256
256
257 def _deprecated_imp(*args, **kwargs):
257 def _deprecated_imp(*args, **kwargs):
258 # Poor man's replacement for the with statement
258 # Poor man's replacement for the with statement
259 ctx = WarningManager(record=True)
259 ctx = WarningManager(record=True)
260 l = ctx.__enter__()
260 l = ctx.__enter__()
261 warnings.simplefilter('always')
261 warnings.simplefilter('always')
262 try:
262 try:
263 f(*args, **kwargs)
263 f(*args, **kwargs)
264 if not len(l) > 0:
264 if not len(l) > 0:
265 raise AssertionError("No warning raised when calling %s"
265 raise AssertionError("No warning raised when calling %s"
266 % f.__name__)
266 % f.__name__)
267 if not l[0].category is DeprecationWarning:
267 if not l[0].category is DeprecationWarning:
268 raise AssertionError("First warning for %s is not a " \
268 raise AssertionError("First warning for %s is not a " \
269 "DeprecationWarning( is %s)" % (f.__name__, l[0]))
269 "DeprecationWarning( is %s)" % (f.__name__, l[0]))
270 finally:
270 finally:
271 ctx.__exit__()
271 ctx.__exit__()
272
272
273 if callable(conditional):
273 if callable(conditional):
274 cond = conditional()
274 cond = conditional()
275 else:
275 else:
276 cond = conditional
276 cond = conditional
277 if cond:
277 if cond:
278 return nose.tools.make_decorator(f)(_deprecated_imp)
278 return nose.tools.make_decorator(f)(_deprecated_imp)
279 else:
279 else:
280 return f
280 return f
281 return deprecate_decorator
281 return deprecate_decorator
@@ -1,4 +1,4 b''
1 try:
1 try:
2 from jsonpointer import *
2 from jsonpointer import *
3 except ImportError :
3 except ImportError :
4 from _jsonpointer import *
4 from ._jsonpointer import *
@@ -1,4 +1,4 b''
1 try:
1 try:
2 from jsonschema import *
2 from jsonschema import *
3 except ImportError :
3 except ImportError :
4 from _jsonschema import *
4 from ._jsonschema import *
@@ -1,5 +1,5 b''
1 try:
1 try:
2 import pexpect
2 import pexpect
3 from pexpect import *
3 from pexpect import *
4 except ImportError:
4 except ImportError:
5 from _pexpect import *
5 from ._pexpect import *
@@ -1,4 +1,4 b''
1 try:
1 try:
2 from simplegeneric import *
2 from simplegeneric import *
3 except ImportError:
3 except ImportError:
4 from _simplegeneric import *
4 from ._simplegeneric import *
@@ -1,356 +1,356 b''
1 """Basic ssh tunnel utilities, and convenience functions for tunneling
1 """Basic ssh tunnel utilities, and convenience functions for tunneling
2 zeromq connections.
2 zeromq connections.
3
3
4 Authors
4 Authors
5 -------
5 -------
6 * Min RK
6 * Min RK
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2010-2011 The IPython Development Team
10 # Copyright (C) 2010-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16
16
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 from __future__ import print_function
22 from __future__ import print_function
23
23
24 import os,sys, atexit
24 import os,sys, atexit
25 import signal
25 import signal
26 import socket
26 import socket
27 from multiprocessing import Process
27 from multiprocessing import Process
28 from getpass import getpass, getuser
28 from getpass import getpass, getuser
29 import warnings
29 import warnings
30
30
31 try:
31 try:
32 with warnings.catch_warnings():
32 with warnings.catch_warnings():
33 warnings.simplefilter('ignore', DeprecationWarning)
33 warnings.simplefilter('ignore', DeprecationWarning)
34 import paramiko
34 import paramiko
35 except ImportError:
35 except ImportError:
36 paramiko = None
36 paramiko = None
37 else:
37 else:
38 from forward import forward_tunnel
38 from .forward import forward_tunnel
39
39
40 try:
40 try:
41 from IPython.external import pexpect
41 from IPython.external import pexpect
42 except ImportError:
42 except ImportError:
43 pexpect = None
43 pexpect = None
44
44
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46 # Code
46 # Code
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48
48
49 # select_random_ports copied from IPython.parallel.util
49 # select_random_ports copied from IPython.parallel.util
50 _random_ports = set()
50 _random_ports = set()
51
51
52 def select_random_ports(n):
52 def select_random_ports(n):
53 """Selects and return n random ports that are available."""
53 """Selects and return n random ports that are available."""
54 ports = []
54 ports = []
55 for i in xrange(n):
55 for i in xrange(n):
56 sock = socket.socket()
56 sock = socket.socket()
57 sock.bind(('', 0))
57 sock.bind(('', 0))
58 while sock.getsockname()[1] in _random_ports:
58 while sock.getsockname()[1] in _random_ports:
59 sock.close()
59 sock.close()
60 sock = socket.socket()
60 sock = socket.socket()
61 sock.bind(('', 0))
61 sock.bind(('', 0))
62 ports.append(sock)
62 ports.append(sock)
63 for i, sock in enumerate(ports):
63 for i, sock in enumerate(ports):
64 port = sock.getsockname()[1]
64 port = sock.getsockname()[1]
65 sock.close()
65 sock.close()
66 ports[i] = port
66 ports[i] = port
67 _random_ports.add(port)
67 _random_ports.add(port)
68 return ports
68 return ports
69
69
70
70
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72 # Check for passwordless login
72 # Check for passwordless login
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74
74
75 def try_passwordless_ssh(server, keyfile, paramiko=None):
75 def try_passwordless_ssh(server, keyfile, paramiko=None):
76 """Attempt to make an ssh connection without a password.
76 """Attempt to make an ssh connection without a password.
77 This is mainly used for requiring password input only once
77 This is mainly used for requiring password input only once
78 when many tunnels may be connected to the same server.
78 when many tunnels may be connected to the same server.
79
79
80 If paramiko is None, the default for the platform is chosen.
80 If paramiko is None, the default for the platform is chosen.
81 """
81 """
82 if paramiko is None:
82 if paramiko is None:
83 paramiko = sys.platform == 'win32'
83 paramiko = sys.platform == 'win32'
84 if not paramiko:
84 if not paramiko:
85 f = _try_passwordless_openssh
85 f = _try_passwordless_openssh
86 else:
86 else:
87 f = _try_passwordless_paramiko
87 f = _try_passwordless_paramiko
88 return f(server, keyfile)
88 return f(server, keyfile)
89
89
90 def _try_passwordless_openssh(server, keyfile):
90 def _try_passwordless_openssh(server, keyfile):
91 """Try passwordless login with shell ssh command."""
91 """Try passwordless login with shell ssh command."""
92 if pexpect is None:
92 if pexpect is None:
93 raise ImportError("pexpect unavailable, use paramiko")
93 raise ImportError("pexpect unavailable, use paramiko")
94 cmd = 'ssh -f '+ server
94 cmd = 'ssh -f '+ server
95 if keyfile:
95 if keyfile:
96 cmd += ' -i ' + keyfile
96 cmd += ' -i ' + keyfile
97 cmd += ' exit'
97 cmd += ' exit'
98 p = pexpect.spawn(cmd)
98 p = pexpect.spawn(cmd)
99 while True:
99 while True:
100 try:
100 try:
101 p.expect('[Pp]assword:', timeout=.1)
101 p.expect('[Pp]assword:', timeout=.1)
102 except pexpect.TIMEOUT:
102 except pexpect.TIMEOUT:
103 continue
103 continue
104 except pexpect.EOF:
104 except pexpect.EOF:
105 return True
105 return True
106 else:
106 else:
107 return False
107 return False
108
108
109 def _try_passwordless_paramiko(server, keyfile):
109 def _try_passwordless_paramiko(server, keyfile):
110 """Try passwordless login with paramiko."""
110 """Try passwordless login with paramiko."""
111 if paramiko is None:
111 if paramiko is None:
112 msg = "Paramiko unavaliable, "
112 msg = "Paramiko unavaliable, "
113 if sys.platform == 'win32':
113 if sys.platform == 'win32':
114 msg += "Paramiko is required for ssh tunneled connections on Windows."
114 msg += "Paramiko is required for ssh tunneled connections on Windows."
115 else:
115 else:
116 msg += "use OpenSSH."
116 msg += "use OpenSSH."
117 raise ImportError(msg)
117 raise ImportError(msg)
118 username, server, port = _split_server(server)
118 username, server, port = _split_server(server)
119 client = paramiko.SSHClient()
119 client = paramiko.SSHClient()
120 client.load_system_host_keys()
120 client.load_system_host_keys()
121 client.set_missing_host_key_policy(paramiko.WarningPolicy())
121 client.set_missing_host_key_policy(paramiko.WarningPolicy())
122 try:
122 try:
123 client.connect(server, port, username=username, key_filename=keyfile,
123 client.connect(server, port, username=username, key_filename=keyfile,
124 look_for_keys=True)
124 look_for_keys=True)
125 except paramiko.AuthenticationException:
125 except paramiko.AuthenticationException:
126 return False
126 return False
127 else:
127 else:
128 client.close()
128 client.close()
129 return True
129 return True
130
130
131
131
132 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
132 def tunnel_connection(socket, addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
133 """Connect a socket to an address via an ssh tunnel.
133 """Connect a socket to an address via an ssh tunnel.
134
134
135 This is a wrapper for socket.connect(addr), when addr is not accessible
135 This is a wrapper for socket.connect(addr), when addr is not accessible
136 from the local machine. It simply creates an ssh tunnel using the remaining args,
136 from the local machine. It simply creates an ssh tunnel using the remaining args,
137 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
137 and calls socket.connect('tcp://localhost:lport') where lport is the randomly
138 selected local port of the tunnel.
138 selected local port of the tunnel.
139
139
140 """
140 """
141 new_url, tunnel = open_tunnel(addr, server, keyfile=keyfile, password=password, paramiko=paramiko, timeout=timeout)
141 new_url, tunnel = open_tunnel(addr, server, keyfile=keyfile, password=password, paramiko=paramiko, timeout=timeout)
142 socket.connect(new_url)
142 socket.connect(new_url)
143 return tunnel
143 return tunnel
144
144
145
145
146 def open_tunnel(addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
146 def open_tunnel(addr, server, keyfile=None, password=None, paramiko=None, timeout=60):
147 """Open a tunneled connection from a 0MQ url.
147 """Open a tunneled connection from a 0MQ url.
148
148
149 For use inside tunnel_connection.
149 For use inside tunnel_connection.
150
150
151 Returns
151 Returns
152 -------
152 -------
153
153
154 (url, tunnel): The 0MQ url that has been forwarded, and the tunnel object
154 (url, tunnel): The 0MQ url that has been forwarded, and the tunnel object
155 """
155 """
156
156
157 lport = select_random_ports(1)[0]
157 lport = select_random_ports(1)[0]
158 transport, addr = addr.split('://')
158 transport, addr = addr.split('://')
159 ip,rport = addr.split(':')
159 ip,rport = addr.split(':')
160 rport = int(rport)
160 rport = int(rport)
161 if paramiko is None:
161 if paramiko is None:
162 paramiko = sys.platform == 'win32'
162 paramiko = sys.platform == 'win32'
163 if paramiko:
163 if paramiko:
164 tunnelf = paramiko_tunnel
164 tunnelf = paramiko_tunnel
165 else:
165 else:
166 tunnelf = openssh_tunnel
166 tunnelf = openssh_tunnel
167
167
168 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password, timeout=timeout)
168 tunnel = tunnelf(lport, rport, server, remoteip=ip, keyfile=keyfile, password=password, timeout=timeout)
169 return 'tcp://127.0.0.1:%i'%lport, tunnel
169 return 'tcp://127.0.0.1:%i'%lport, tunnel
170
170
171 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
171 def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
172 """Create an ssh tunnel using command-line ssh that connects port lport
172 """Create an ssh tunnel using command-line ssh that connects port lport
173 on this machine to localhost:rport on server. The tunnel
173 on this machine to localhost:rport on server. The tunnel
174 will automatically close when not in use, remaining open
174 will automatically close when not in use, remaining open
175 for a minimum of timeout seconds for an initial connection.
175 for a minimum of timeout seconds for an initial connection.
176
176
177 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
177 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
178 as seen from `server`.
178 as seen from `server`.
179
179
180 keyfile and password may be specified, but ssh config is checked for defaults.
180 keyfile and password may be specified, but ssh config is checked for defaults.
181
181
182 Parameters
182 Parameters
183 ----------
183 ----------
184
184
185 lport : int
185 lport : int
186 local port for connecting to the tunnel from this machine.
186 local port for connecting to the tunnel from this machine.
187 rport : int
187 rport : int
188 port on the remote machine to connect to.
188 port on the remote machine to connect to.
189 server : str
189 server : str
190 The ssh server to connect to. The full ssh server string will be parsed.
190 The ssh server to connect to. The full ssh server string will be parsed.
191 user@server:port
191 user@server:port
192 remoteip : str [Default: 127.0.0.1]
192 remoteip : str [Default: 127.0.0.1]
193 The remote ip, specifying the destination of the tunnel.
193 The remote ip, specifying the destination of the tunnel.
194 Default is localhost, which means that the tunnel would redirect
194 Default is localhost, which means that the tunnel would redirect
195 localhost:lport on this machine to localhost:rport on the *server*.
195 localhost:lport on this machine to localhost:rport on the *server*.
196
196
197 keyfile : str; path to public key file
197 keyfile : str; path to public key file
198 This specifies a key to be used in ssh login, default None.
198 This specifies a key to be used in ssh login, default None.
199 Regular default ssh keys will be used without specifying this argument.
199 Regular default ssh keys will be used without specifying this argument.
200 password : str;
200 password : str;
201 Your ssh password to the ssh server. Note that if this is left None,
201 Your ssh password to the ssh server. Note that if this is left None,
202 you will be prompted for it if passwordless key based login is unavailable.
202 you will be prompted for it if passwordless key based login is unavailable.
203 timeout : int [default: 60]
203 timeout : int [default: 60]
204 The time (in seconds) after which no activity will result in the tunnel
204 The time (in seconds) after which no activity will result in the tunnel
205 closing. This prevents orphaned tunnels from running forever.
205 closing. This prevents orphaned tunnels from running forever.
206 """
206 """
207 if pexpect is None:
207 if pexpect is None:
208 raise ImportError("pexpect unavailable, use paramiko_tunnel")
208 raise ImportError("pexpect unavailable, use paramiko_tunnel")
209 ssh="ssh "
209 ssh="ssh "
210 if keyfile:
210 if keyfile:
211 ssh += "-i " + keyfile
211 ssh += "-i " + keyfile
212
212
213 if ':' in server:
213 if ':' in server:
214 server, port = server.split(':')
214 server, port = server.split(':')
215 ssh += " -p %s" % port
215 ssh += " -p %s" % port
216
216
217 cmd = "%s -f -L 127.0.0.1:%i:%s:%i %s sleep %i" % (
217 cmd = "%s -f -L 127.0.0.1:%i:%s:%i %s sleep %i" % (
218 ssh, lport, remoteip, rport, server, timeout)
218 ssh, lport, remoteip, rport, server, timeout)
219 tunnel = pexpect.spawn(cmd)
219 tunnel = pexpect.spawn(cmd)
220 failed = False
220 failed = False
221 while True:
221 while True:
222 try:
222 try:
223 tunnel.expect('[Pp]assword:', timeout=.1)
223 tunnel.expect('[Pp]assword:', timeout=.1)
224 except pexpect.TIMEOUT:
224 except pexpect.TIMEOUT:
225 continue
225 continue
226 except pexpect.EOF:
226 except pexpect.EOF:
227 if tunnel.exitstatus:
227 if tunnel.exitstatus:
228 print (tunnel.exitstatus)
228 print (tunnel.exitstatus)
229 print (tunnel.before)
229 print (tunnel.before)
230 print (tunnel.after)
230 print (tunnel.after)
231 raise RuntimeError("tunnel '%s' failed to start"%(cmd))
231 raise RuntimeError("tunnel '%s' failed to start"%(cmd))
232 else:
232 else:
233 return tunnel.pid
233 return tunnel.pid
234 else:
234 else:
235 if failed:
235 if failed:
236 print("Password rejected, try again")
236 print("Password rejected, try again")
237 password=None
237 password=None
238 if password is None:
238 if password is None:
239 password = getpass("%s's password: "%(server))
239 password = getpass("%s's password: "%(server))
240 tunnel.sendline(password)
240 tunnel.sendline(password)
241 failed = True
241 failed = True
242
242
243 def _split_server(server):
243 def _split_server(server):
244 if '@' in server:
244 if '@' in server:
245 username,server = server.split('@', 1)
245 username,server = server.split('@', 1)
246 else:
246 else:
247 username = getuser()
247 username = getuser()
248 if ':' in server:
248 if ':' in server:
249 server, port = server.split(':')
249 server, port = server.split(':')
250 port = int(port)
250 port = int(port)
251 else:
251 else:
252 port = 22
252 port = 22
253 return username, server, port
253 return username, server, port
254
254
255 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
255 def paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, password=None, timeout=60):
256 """launch a tunner with paramiko in a subprocess. This should only be used
256 """launch a tunner with paramiko in a subprocess. This should only be used
257 when shell ssh is unavailable (e.g. Windows).
257 when shell ssh is unavailable (e.g. Windows).
258
258
259 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
259 This creates a tunnel redirecting `localhost:lport` to `remoteip:rport`,
260 as seen from `server`.
260 as seen from `server`.
261
261
262 If you are familiar with ssh tunnels, this creates the tunnel:
262 If you are familiar with ssh tunnels, this creates the tunnel:
263
263
264 ssh server -L localhost:lport:remoteip:rport
264 ssh server -L localhost:lport:remoteip:rport
265
265
266 keyfile and password may be specified, but ssh config is checked for defaults.
266 keyfile and password may be specified, but ssh config is checked for defaults.
267
267
268
268
269 Parameters
269 Parameters
270 ----------
270 ----------
271
271
272 lport : int
272 lport : int
273 local port for connecting to the tunnel from this machine.
273 local port for connecting to the tunnel from this machine.
274 rport : int
274 rport : int
275 port on the remote machine to connect to.
275 port on the remote machine to connect to.
276 server : str
276 server : str
277 The ssh server to connect to. The full ssh server string will be parsed.
277 The ssh server to connect to. The full ssh server string will be parsed.
278 user@server:port
278 user@server:port
279 remoteip : str [Default: 127.0.0.1]
279 remoteip : str [Default: 127.0.0.1]
280 The remote ip, specifying the destination of the tunnel.
280 The remote ip, specifying the destination of the tunnel.
281 Default is localhost, which means that the tunnel would redirect
281 Default is localhost, which means that the tunnel would redirect
282 localhost:lport on this machine to localhost:rport on the *server*.
282 localhost:lport on this machine to localhost:rport on the *server*.
283
283
284 keyfile : str; path to public key file
284 keyfile : str; path to public key file
285 This specifies a key to be used in ssh login, default None.
285 This specifies a key to be used in ssh login, default None.
286 Regular default ssh keys will be used without specifying this argument.
286 Regular default ssh keys will be used without specifying this argument.
287 password : str;
287 password : str;
288 Your ssh password to the ssh server. Note that if this is left None,
288 Your ssh password to the ssh server. Note that if this is left None,
289 you will be prompted for it if passwordless key based login is unavailable.
289 you will be prompted for it if passwordless key based login is unavailable.
290 timeout : int [default: 60]
290 timeout : int [default: 60]
291 The time (in seconds) after which no activity will result in the tunnel
291 The time (in seconds) after which no activity will result in the tunnel
292 closing. This prevents orphaned tunnels from running forever.
292 closing. This prevents orphaned tunnels from running forever.
293
293
294 """
294 """
295 if paramiko is None:
295 if paramiko is None:
296 raise ImportError("Paramiko not available")
296 raise ImportError("Paramiko not available")
297
297
298 if password is None:
298 if password is None:
299 if not _try_passwordless_paramiko(server, keyfile):
299 if not _try_passwordless_paramiko(server, keyfile):
300 password = getpass("%s's password: "%(server))
300 password = getpass("%s's password: "%(server))
301
301
302 p = Process(target=_paramiko_tunnel,
302 p = Process(target=_paramiko_tunnel,
303 args=(lport, rport, server, remoteip),
303 args=(lport, rport, server, remoteip),
304 kwargs=dict(keyfile=keyfile, password=password))
304 kwargs=dict(keyfile=keyfile, password=password))
305 p.daemon=False
305 p.daemon=False
306 p.start()
306 p.start()
307 atexit.register(_shutdown_process, p)
307 atexit.register(_shutdown_process, p)
308 return p
308 return p
309
309
310 def _shutdown_process(p):
310 def _shutdown_process(p):
311 if p.is_alive():
311 if p.is_alive():
312 p.terminate()
312 p.terminate()
313
313
314 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
314 def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None):
315 """Function for actually starting a paramiko tunnel, to be passed
315 """Function for actually starting a paramiko tunnel, to be passed
316 to multiprocessing.Process(target=this), and not called directly.
316 to multiprocessing.Process(target=this), and not called directly.
317 """
317 """
318 username, server, port = _split_server(server)
318 username, server, port = _split_server(server)
319 client = paramiko.SSHClient()
319 client = paramiko.SSHClient()
320 client.load_system_host_keys()
320 client.load_system_host_keys()
321 client.set_missing_host_key_policy(paramiko.WarningPolicy())
321 client.set_missing_host_key_policy(paramiko.WarningPolicy())
322
322
323 try:
323 try:
324 client.connect(server, port, username=username, key_filename=keyfile,
324 client.connect(server, port, username=username, key_filename=keyfile,
325 look_for_keys=True, password=password)
325 look_for_keys=True, password=password)
326 # except paramiko.AuthenticationException:
326 # except paramiko.AuthenticationException:
327 # if password is None:
327 # if password is None:
328 # password = getpass("%s@%s's password: "%(username, server))
328 # password = getpass("%s@%s's password: "%(username, server))
329 # client.connect(server, port, username=username, password=password)
329 # client.connect(server, port, username=username, password=password)
330 # else:
330 # else:
331 # raise
331 # raise
332 except Exception as e:
332 except Exception as e:
333 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
333 print ('*** Failed to connect to %s:%d: %r' % (server, port, e))
334 sys.exit(1)
334 sys.exit(1)
335
335
336 # Don't let SIGINT kill the tunnel subprocess
336 # Don't let SIGINT kill the tunnel subprocess
337 signal.signal(signal.SIGINT, signal.SIG_IGN)
337 signal.signal(signal.SIGINT, signal.SIG_IGN)
338
338
339 try:
339 try:
340 forward_tunnel(lport, remoteip, rport, client.get_transport())
340 forward_tunnel(lport, remoteip, rport, client.get_transport())
341 except KeyboardInterrupt:
341 except KeyboardInterrupt:
342 print ('SIGINT: Port forwarding stopped cleanly')
342 print ('SIGINT: Port forwarding stopped cleanly')
343 sys.exit(0)
343 sys.exit(0)
344 except Exception as e:
344 except Exception as e:
345 print ("Port forwarding stopped uncleanly: %s"%e)
345 print ("Port forwarding stopped uncleanly: %s"%e)
346 sys.exit(255)
346 sys.exit(255)
347
347
348 if sys.platform == 'win32':
348 if sys.platform == 'win32':
349 ssh_tunnel = paramiko_tunnel
349 ssh_tunnel = paramiko_tunnel
350 else:
350 else:
351 ssh_tunnel = openssh_tunnel
351 ssh_tunnel = openssh_tunnel
352
352
353
353
354 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
354 __all__ = ['tunnel_connection', 'ssh_tunnel', 'openssh_tunnel', 'paramiko_tunnel', 'try_passwordless_ssh']
355
355
356
356
@@ -1,65 +1,65 b''
1 import __builtin__
1 import __builtin__
2 import sys
2 import sys
3
3
4 from IPython.core.displayhook import DisplayHook
4 from IPython.core.displayhook import DisplayHook
5 from IPython.kernel.inprocess.socket import SocketABC
5 from IPython.kernel.inprocess.socket import SocketABC
6 from IPython.utils.jsonutil import encode_images
6 from IPython.utils.jsonutil import encode_images
7 from IPython.utils.traitlets import Instance, Dict
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 class ZMQDisplayHook(object):
10 class ZMQDisplayHook(object):
11 """A simple displayhook that publishes the object's repr over a ZeroMQ
11 """A simple displayhook that publishes the object's repr over a ZeroMQ
12 socket."""
12 socket."""
13 topic=b'pyout'
13 topic=b'pyout'
14
14
15 def __init__(self, session, pub_socket):
15 def __init__(self, session, pub_socket):
16 self.session = session
16 self.session = session
17 self.pub_socket = pub_socket
17 self.pub_socket = pub_socket
18 self.parent_header = {}
18 self.parent_header = {}
19
19
20 def __call__(self, obj):
20 def __call__(self, obj):
21 if obj is None:
21 if obj is None:
22 return
22 return
23
23
24 __builtin__._ = obj
24 __builtin__._ = obj
25 sys.stdout.flush()
25 sys.stdout.flush()
26 sys.stderr.flush()
26 sys.stderr.flush()
27 msg = self.session.send(self.pub_socket, u'pyout', {u'data':repr(obj)},
27 msg = self.session.send(self.pub_socket, u'pyout', {u'data':repr(obj)},
28 parent=self.parent_header, ident=self.topic)
28 parent=self.parent_header, ident=self.topic)
29
29
30 def set_parent(self, parent):
30 def set_parent(self, parent):
31 self.parent_header = extract_header(parent)
31 self.parent_header = extract_header(parent)
32
32
33
33
34 class ZMQShellDisplayHook(DisplayHook):
34 class ZMQShellDisplayHook(DisplayHook):
35 """A displayhook subclass that publishes data using ZeroMQ. This is intended
35 """A displayhook subclass that publishes data using ZeroMQ. This is intended
36 to work with an InteractiveShell instance. It sends a dict of different
36 to work with an InteractiveShell instance. It sends a dict of different
37 representations of the object."""
37 representations of the object."""
38 topic=None
38 topic=None
39
39
40 session = Instance(Session)
40 session = Instance(Session)
41 pub_socket = Instance(SocketABC)
41 pub_socket = Instance(SocketABC)
42 parent_header = Dict({})
42 parent_header = Dict({})
43
43
44 def set_parent(self, parent):
44 def set_parent(self, parent):
45 """Set the parent for outbound messages."""
45 """Set the parent for outbound messages."""
46 self.parent_header = extract_header(parent)
46 self.parent_header = extract_header(parent)
47
47
48 def start_displayhook(self):
48 def start_displayhook(self):
49 self.msg = self.session.msg(u'pyout', {}, parent=self.parent_header)
49 self.msg = self.session.msg(u'pyout', {}, parent=self.parent_header)
50
50
51 def write_output_prompt(self):
51 def write_output_prompt(self):
52 """Write the output prompt."""
52 """Write the output prompt."""
53 self.msg['content']['execution_count'] = self.prompt_count
53 self.msg['content']['execution_count'] = self.prompt_count
54
54
55 def write_format_data(self, format_dict, md_dict=None):
55 def write_format_data(self, format_dict, md_dict=None):
56 self.msg['content']['data'] = encode_images(format_dict)
56 self.msg['content']['data'] = encode_images(format_dict)
57 self.msg['content']['metadata'] = md_dict
57 self.msg['content']['metadata'] = md_dict
58
58
59 def finish_displayhook(self):
59 def finish_displayhook(self):
60 """Finish up all displayhook activities."""
60 """Finish up all displayhook activities."""
61 sys.stdout.flush()
61 sys.stdout.flush()
62 sys.stderr.flush()
62 sys.stderr.flush()
63 self.session.send(self.pub_socket, self.msg, ident=self.topic)
63 self.session.send(self.pub_socket, self.msg, ident=self.topic)
64 self.msg = None
64 self.msg = None
65
65
@@ -1,57 +1,57 b''
1 """Simple function for embedding an IPython kernel
1 """Simple function for embedding an IPython kernel
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 import sys
7 import sys
8
8
9 from IPython.utils.frame import extract_module_locals
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 # Code
14 # Code
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 def embed_kernel(module=None, local_ns=None, **kwargs):
17 def embed_kernel(module=None, local_ns=None, **kwargs):
18 """Embed and start an IPython kernel in a given scope.
18 """Embed and start an IPython kernel in a given scope.
19
19
20 Parameters
20 Parameters
21 ----------
21 ----------
22 module : ModuleType, optional
22 module : ModuleType, optional
23 The module to load into IPython globals (default: caller)
23 The module to load into IPython globals (default: caller)
24 local_ns : dict, optional
24 local_ns : dict, optional
25 The namespace to load into IPython user namespace (default: caller)
25 The namespace to load into IPython user namespace (default: caller)
26
26
27 kwargs : various, optional
27 kwargs : various, optional
28 Further keyword args are relayed to the IPKernelApp constructor,
28 Further keyword args are relayed to the IPKernelApp constructor,
29 allowing configuration of the Kernel. Will only have an effect
29 allowing configuration of the Kernel. Will only have an effect
30 on the first embed_kernel call for a given process.
30 on the first embed_kernel call for a given process.
31
31
32 """
32 """
33 # get the app if it exists, or set it up if it doesn't
33 # get the app if it exists, or set it up if it doesn't
34 if IPKernelApp.initialized():
34 if IPKernelApp.initialized():
35 app = IPKernelApp.instance()
35 app = IPKernelApp.instance()
36 else:
36 else:
37 app = IPKernelApp.instance(**kwargs)
37 app = IPKernelApp.instance(**kwargs)
38 app.initialize([])
38 app.initialize([])
39 # Undo unnecessary sys module mangling from init_sys_modules.
39 # Undo unnecessary sys module mangling from init_sys_modules.
40 # This would not be necessary if we could prevent it
40 # This would not be necessary if we could prevent it
41 # in the first place by using a different InteractiveShell
41 # in the first place by using a different InteractiveShell
42 # subclass, as in the regular embed case.
42 # subclass, as in the regular embed case.
43 main = app.kernel.shell._orig_sys_modules_main_mod
43 main = app.kernel.shell._orig_sys_modules_main_mod
44 if main is not None:
44 if main is not None:
45 sys.modules[app.kernel.shell._orig_sys_modules_main_name] = main
45 sys.modules[app.kernel.shell._orig_sys_modules_main_name] = main
46
46
47 # load the calling scope if not given
47 # load the calling scope if not given
48 (caller_module, caller_locals) = extract_module_locals(1)
48 (caller_module, caller_locals) = extract_module_locals(1)
49 if module is None:
49 if module is None:
50 module = caller_module
50 module = caller_module
51 if local_ns is None:
51 if local_ns is None:
52 local_ns = caller_locals
52 local_ns = caller_locals
53
53
54 app.kernel.user_module = module
54 app.kernel.user_module = module
55 app.kernel.user_ns = local_ns
55 app.kernel.user_ns = local_ns
56 app.shell.set_completer_frame()
56 app.shell.set_completer_frame()
57 app.start()
57 app.start()
@@ -1,220 +1,220 b''
1 """wrappers for stdout/stderr forwarding over zmq
1 """wrappers for stdout/stderr forwarding over zmq
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2013 The IPython Development Team
5 # Copyright (C) 2013 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 import os
11 import os
12 import threading
12 import threading
13 import time
13 import time
14 import uuid
14 import uuid
15 from io import StringIO, UnsupportedOperation
15 from io import StringIO, UnsupportedOperation
16
16
17 import zmq
17 import zmq
18
18
19 from session import extract_header
19 from .session import extract_header
20
20
21 from IPython.utils import py3compat
21 from IPython.utils import py3compat
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Globals
24 # Globals
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 MASTER = 0
27 MASTER = 0
28 CHILD = 1
28 CHILD = 1
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Stream classes
31 # Stream classes
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34 class OutStream(object):
34 class OutStream(object):
35 """A file like object that publishes the stream to a 0MQ PUB socket."""
35 """A file like object that publishes the stream to a 0MQ PUB socket."""
36
36
37 # The time interval between automatic flushes, in seconds.
37 # The time interval between automatic flushes, in seconds.
38 _subprocess_flush_limit = 256
38 _subprocess_flush_limit = 256
39 flush_interval = 0.05
39 flush_interval = 0.05
40 topic=None
40 topic=None
41
41
42 def __init__(self, session, pub_socket, name, pipe=True):
42 def __init__(self, session, pub_socket, name, pipe=True):
43 self.encoding = 'UTF-8'
43 self.encoding = 'UTF-8'
44 self.session = session
44 self.session = session
45 self.pub_socket = pub_socket
45 self.pub_socket = pub_socket
46 self.name = name
46 self.name = name
47 self.topic = b'stream.' + py3compat.cast_bytes(name)
47 self.topic = b'stream.' + py3compat.cast_bytes(name)
48 self.parent_header = {}
48 self.parent_header = {}
49 self._new_buffer()
49 self._new_buffer()
50 self._buffer_lock = threading.Lock()
50 self._buffer_lock = threading.Lock()
51 self._master_pid = os.getpid()
51 self._master_pid = os.getpid()
52 self._master_thread = threading.current_thread().ident
52 self._master_thread = threading.current_thread().ident
53 self._pipe_pid = os.getpid()
53 self._pipe_pid = os.getpid()
54 self._pipe_flag = pipe
54 self._pipe_flag = pipe
55 if pipe:
55 if pipe:
56 self._setup_pipe_in()
56 self._setup_pipe_in()
57
57
58 def _setup_pipe_in(self):
58 def _setup_pipe_in(self):
59 """setup listening pipe for subprocesses"""
59 """setup listening pipe for subprocesses"""
60 ctx = self.pub_socket.context
60 ctx = self.pub_socket.context
61
61
62 # use UUID to authenticate pipe messages
62 # use UUID to authenticate pipe messages
63 self._pipe_uuid = uuid.uuid4().bytes
63 self._pipe_uuid = uuid.uuid4().bytes
64
64
65 self._pipe_in = ctx.socket(zmq.PULL)
65 self._pipe_in = ctx.socket(zmq.PULL)
66 self._pipe_in.linger = 0
66 self._pipe_in.linger = 0
67 self._pipe_port = self._pipe_in.bind_to_random_port("tcp://127.0.0.1")
67 self._pipe_port = self._pipe_in.bind_to_random_port("tcp://127.0.0.1")
68 self._pipe_poller = zmq.Poller()
68 self._pipe_poller = zmq.Poller()
69 self._pipe_poller.register(self._pipe_in, zmq.POLLIN)
69 self._pipe_poller.register(self._pipe_in, zmq.POLLIN)
70
70
71 def _setup_pipe_out(self):
71 def _setup_pipe_out(self):
72 # must be new context after fork
72 # must be new context after fork
73 ctx = zmq.Context()
73 ctx = zmq.Context()
74 self._pipe_pid = os.getpid()
74 self._pipe_pid = os.getpid()
75 self._pipe_out = ctx.socket(zmq.PUSH)
75 self._pipe_out = ctx.socket(zmq.PUSH)
76 self._pipe_out_lock = threading.Lock()
76 self._pipe_out_lock = threading.Lock()
77 self._pipe_out.connect("tcp://127.0.0.1:%i" % self._pipe_port)
77 self._pipe_out.connect("tcp://127.0.0.1:%i" % self._pipe_port)
78
78
79 def _is_master_process(self):
79 def _is_master_process(self):
80 return os.getpid() == self._master_pid
80 return os.getpid() == self._master_pid
81
81
82 def _is_master_thread(self):
82 def _is_master_thread(self):
83 return threading.current_thread().ident == self._master_thread
83 return threading.current_thread().ident == self._master_thread
84
84
85 def _have_pipe_out(self):
85 def _have_pipe_out(self):
86 return os.getpid() == self._pipe_pid
86 return os.getpid() == self._pipe_pid
87
87
88 def _check_mp_mode(self):
88 def _check_mp_mode(self):
89 """check for forks, and switch to zmq pipeline if necessary"""
89 """check for forks, and switch to zmq pipeline if necessary"""
90 if not self._pipe_flag or self._is_master_process():
90 if not self._pipe_flag or self._is_master_process():
91 return MASTER
91 return MASTER
92 else:
92 else:
93 if not self._have_pipe_out():
93 if not self._have_pipe_out():
94 self._flush_buffer()
94 self._flush_buffer()
95 # setup a new out pipe
95 # setup a new out pipe
96 self._setup_pipe_out()
96 self._setup_pipe_out()
97 return CHILD
97 return CHILD
98
98
99 def set_parent(self, parent):
99 def set_parent(self, parent):
100 self.parent_header = extract_header(parent)
100 self.parent_header = extract_header(parent)
101
101
102 def close(self):
102 def close(self):
103 self.pub_socket = None
103 self.pub_socket = None
104
104
105 def _flush_from_subprocesses(self):
105 def _flush_from_subprocesses(self):
106 """flush possible pub data from subprocesses into my buffer"""
106 """flush possible pub data from subprocesses into my buffer"""
107 if not self._pipe_flag or not self._is_master_process():
107 if not self._pipe_flag or not self._is_master_process():
108 return
108 return
109 for i in range(self._subprocess_flush_limit):
109 for i in range(self._subprocess_flush_limit):
110 if self._pipe_poller.poll(0):
110 if self._pipe_poller.poll(0):
111 msg = self._pipe_in.recv_multipart()
111 msg = self._pipe_in.recv_multipart()
112 if msg[0] != self._pipe_uuid:
112 if msg[0] != self._pipe_uuid:
113 continue
113 continue
114 else:
114 else:
115 self._buffer.write(msg[1].decode(self.encoding, 'replace'))
115 self._buffer.write(msg[1].decode(self.encoding, 'replace'))
116 # this always means a flush,
116 # this always means a flush,
117 # so reset our timer
117 # so reset our timer
118 self._start = 0
118 self._start = 0
119 else:
119 else:
120 break
120 break
121
121
122 def flush(self):
122 def flush(self):
123 """trigger actual zmq send"""
123 """trigger actual zmq send"""
124 if self.pub_socket is None:
124 if self.pub_socket is None:
125 raise ValueError(u'I/O operation on closed file')
125 raise ValueError(u'I/O operation on closed file')
126
126
127 mp_mode = self._check_mp_mode()
127 mp_mode = self._check_mp_mode()
128
128
129 if mp_mode != CHILD:
129 if mp_mode != CHILD:
130 # we are master
130 # we are master
131 if not self._is_master_thread():
131 if not self._is_master_thread():
132 # sub-threads must not trigger flush,
132 # sub-threads must not trigger flush,
133 # but at least they can force the timer.
133 # but at least they can force the timer.
134 self._start = 0
134 self._start = 0
135 return
135 return
136
136
137 self._flush_from_subprocesses()
137 self._flush_from_subprocesses()
138 data = self._flush_buffer()
138 data = self._flush_buffer()
139
139
140 if data:
140 if data:
141 content = {u'name':self.name, u'data':data}
141 content = {u'name':self.name, u'data':data}
142 msg = self.session.send(self.pub_socket, u'stream', content=content,
142 msg = self.session.send(self.pub_socket, u'stream', content=content,
143 parent=self.parent_header, ident=self.topic)
143 parent=self.parent_header, ident=self.topic)
144
144
145 if hasattr(self.pub_socket, 'flush'):
145 if hasattr(self.pub_socket, 'flush'):
146 # socket itself has flush (presumably ZMQStream)
146 # socket itself has flush (presumably ZMQStream)
147 self.pub_socket.flush()
147 self.pub_socket.flush()
148 else:
148 else:
149 with self._pipe_out_lock:
149 with self._pipe_out_lock:
150 string = self._flush_buffer()
150 string = self._flush_buffer()
151 tracker = self._pipe_out.send_multipart([
151 tracker = self._pipe_out.send_multipart([
152 self._pipe_uuid,
152 self._pipe_uuid,
153 string.encode(self.encoding, 'replace'),
153 string.encode(self.encoding, 'replace'),
154 ], copy=False, track=True)
154 ], copy=False, track=True)
155 try:
155 try:
156 tracker.wait(1)
156 tracker.wait(1)
157 except:
157 except:
158 pass
158 pass
159
159
160 def isatty(self):
160 def isatty(self):
161 return False
161 return False
162
162
163 def __next__(self):
163 def __next__(self):
164 raise IOError('Read not supported on a write only stream.')
164 raise IOError('Read not supported on a write only stream.')
165
165
166 if not py3compat.PY3:
166 if not py3compat.PY3:
167 next = __next__
167 next = __next__
168
168
169 def read(self, size=-1):
169 def read(self, size=-1):
170 raise IOError('Read not supported on a write only stream.')
170 raise IOError('Read not supported on a write only stream.')
171
171
172 def readline(self, size=-1):
172 def readline(self, size=-1):
173 raise IOError('Read not supported on a write only stream.')
173 raise IOError('Read not supported on a write only stream.')
174
174
175 def fileno(self):
175 def fileno(self):
176 raise UnsupportedOperation("IOStream has no fileno.")
176 raise UnsupportedOperation("IOStream has no fileno.")
177
177
178 def write(self, string):
178 def write(self, string):
179 if self.pub_socket is None:
179 if self.pub_socket is None:
180 raise ValueError('I/O operation on closed file')
180 raise ValueError('I/O operation on closed file')
181 else:
181 else:
182 # Make sure that we're handling unicode
182 # Make sure that we're handling unicode
183 if not isinstance(string, unicode):
183 if not isinstance(string, unicode):
184 string = string.decode(self.encoding, 'replace')
184 string = string.decode(self.encoding, 'replace')
185
185
186 is_child = (self._check_mp_mode() == CHILD)
186 is_child = (self._check_mp_mode() == CHILD)
187 self._buffer.write(string)
187 self._buffer.write(string)
188 if is_child:
188 if is_child:
189 # newlines imply flush in subprocesses
189 # newlines imply flush in subprocesses
190 # mp.Pool cannot be trusted to flush promptly (or ever),
190 # mp.Pool cannot be trusted to flush promptly (or ever),
191 # and this helps.
191 # and this helps.
192 if '\n' in string:
192 if '\n' in string:
193 self.flush()
193 self.flush()
194 # do we want to check subprocess flushes on write?
194 # do we want to check subprocess flushes on write?
195 # self._flush_from_subprocesses()
195 # self._flush_from_subprocesses()
196 current_time = time.time()
196 current_time = time.time()
197 if self._start < 0:
197 if self._start < 0:
198 self._start = current_time
198 self._start = current_time
199 elif current_time - self._start > self.flush_interval:
199 elif current_time - self._start > self.flush_interval:
200 self.flush()
200 self.flush()
201
201
202 def writelines(self, sequence):
202 def writelines(self, sequence):
203 if self.pub_socket is None:
203 if self.pub_socket is None:
204 raise ValueError('I/O operation on closed file')
204 raise ValueError('I/O operation on closed file')
205 else:
205 else:
206 for string in sequence:
206 for string in sequence:
207 self.write(string)
207 self.write(string)
208
208
209 def _flush_buffer(self):
209 def _flush_buffer(self):
210 """clear the current buffer and return the current buffer data"""
210 """clear the current buffer and return the current buffer data"""
211 data = u''
211 data = u''
212 if self._buffer is not None:
212 if self._buffer is not None:
213 data = self._buffer.getvalue()
213 data = self._buffer.getvalue()
214 self._buffer.close()
214 self._buffer.close()
215 self._new_buffer()
215 self._new_buffer()
216 return data
216 return data
217
217
218 def _new_buffer(self):
218 def _new_buffer(self):
219 self._buffer = StringIO()
219 self._buffer = StringIO()
220 self._start = -1
220 self._start = -1
@@ -1,789 +1,789 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """An interactive kernel that talks to frontends over 0MQ."""
2 """An interactive kernel that talks to frontends over 0MQ."""
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Imports
5 # Imports
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 # Standard library imports
9 # Standard library imports
10 import __builtin__
10 import __builtin__
11 import sys
11 import sys
12 import time
12 import time
13 import traceback
13 import traceback
14 import logging
14 import logging
15 import uuid
15 import uuid
16
16
17 from datetime import datetime
17 from datetime import datetime
18 from signal import (
18 from signal import (
19 signal, default_int_handler, SIGINT
19 signal, default_int_handler, SIGINT
20 )
20 )
21
21
22 # System library imports
22 # System library imports
23 import zmq
23 import zmq
24 from zmq.eventloop import ioloop
24 from zmq.eventloop import ioloop
25 from zmq.eventloop.zmqstream import ZMQStream
25 from zmq.eventloop.zmqstream import ZMQStream
26
26
27 # Local imports
27 # Local imports
28 from IPython.config.configurable import Configurable
28 from IPython.config.configurable import Configurable
29 from IPython.core.error import StdinNotImplementedError
29 from IPython.core.error import StdinNotImplementedError
30 from IPython.core import release
30 from IPython.core import release
31 from IPython.utils import py3compat
31 from IPython.utils import py3compat
32 from IPython.utils.jsonutil import json_clean
32 from IPython.utils.jsonutil import json_clean
33 from IPython.utils.traitlets import (
33 from IPython.utils.traitlets import (
34 Any, Instance, Float, Dict, List, Set, Integer, Unicode,
34 Any, Instance, Float, Dict, List, Set, Integer, Unicode,
35 Type
35 Type
36 )
36 )
37
37
38 from serialize import serialize_object, unpack_apply_message
38 from .serialize import serialize_object, unpack_apply_message
39 from session import Session
39 from .session import Session
40 from zmqshell import ZMQInteractiveShell
40 from .zmqshell import ZMQInteractiveShell
41
41
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Main kernel class
44 # Main kernel class
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 protocol_version = list(release.kernel_protocol_version_info)
47 protocol_version = list(release.kernel_protocol_version_info)
48 ipython_version = list(release.version_info)
48 ipython_version = list(release.version_info)
49 language_version = list(sys.version_info[:3])
49 language_version = list(sys.version_info[:3])
50
50
51
51
52 class Kernel(Configurable):
52 class Kernel(Configurable):
53
53
54 #---------------------------------------------------------------------------
54 #---------------------------------------------------------------------------
55 # Kernel interface
55 # Kernel interface
56 #---------------------------------------------------------------------------
56 #---------------------------------------------------------------------------
57
57
58 # attribute to override with a GUI
58 # attribute to override with a GUI
59 eventloop = Any(None)
59 eventloop = Any(None)
60 def _eventloop_changed(self, name, old, new):
60 def _eventloop_changed(self, name, old, new):
61 """schedule call to eventloop from IOLoop"""
61 """schedule call to eventloop from IOLoop"""
62 loop = ioloop.IOLoop.instance()
62 loop = ioloop.IOLoop.instance()
63 loop.add_timeout(time.time()+0.1, self.enter_eventloop)
63 loop.add_timeout(time.time()+0.1, self.enter_eventloop)
64
64
65 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
65 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
66 shell_class = Type(ZMQInteractiveShell)
66 shell_class = Type(ZMQInteractiveShell)
67
67
68 session = Instance(Session)
68 session = Instance(Session)
69 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
69 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
70 shell_streams = List()
70 shell_streams = List()
71 control_stream = Instance(ZMQStream)
71 control_stream = Instance(ZMQStream)
72 iopub_socket = Instance(zmq.Socket)
72 iopub_socket = Instance(zmq.Socket)
73 stdin_socket = Instance(zmq.Socket)
73 stdin_socket = Instance(zmq.Socket)
74 log = Instance(logging.Logger)
74 log = Instance(logging.Logger)
75
75
76 user_module = Any()
76 user_module = Any()
77 def _user_module_changed(self, name, old, new):
77 def _user_module_changed(self, name, old, new):
78 if self.shell is not None:
78 if self.shell is not None:
79 self.shell.user_module = new
79 self.shell.user_module = new
80
80
81 user_ns = Instance(dict, args=None, allow_none=True)
81 user_ns = Instance(dict, args=None, allow_none=True)
82 def _user_ns_changed(self, name, old, new):
82 def _user_ns_changed(self, name, old, new):
83 if self.shell is not None:
83 if self.shell is not None:
84 self.shell.user_ns = new
84 self.shell.user_ns = new
85 self.shell.init_user_ns()
85 self.shell.init_user_ns()
86
86
87 # identities:
87 # identities:
88 int_id = Integer(-1)
88 int_id = Integer(-1)
89 ident = Unicode()
89 ident = Unicode()
90
90
91 def _ident_default(self):
91 def _ident_default(self):
92 return unicode(uuid.uuid4())
92 return unicode(uuid.uuid4())
93
93
94
94
95 # Private interface
95 # Private interface
96
96
97 # Time to sleep after flushing the stdout/err buffers in each execute
97 # Time to sleep after flushing the stdout/err buffers in each execute
98 # cycle. While this introduces a hard limit on the minimal latency of the
98 # cycle. While this introduces a hard limit on the minimal latency of the
99 # execute cycle, it helps prevent output synchronization problems for
99 # execute cycle, it helps prevent output synchronization problems for
100 # clients.
100 # clients.
101 # Units are in seconds. The minimum zmq latency on local host is probably
101 # Units are in seconds. The minimum zmq latency on local host is probably
102 # ~150 microseconds, set this to 500us for now. We may need to increase it
102 # ~150 microseconds, set this to 500us for now. We may need to increase it
103 # a little if it's not enough after more interactive testing.
103 # a little if it's not enough after more interactive testing.
104 _execute_sleep = Float(0.0005, config=True)
104 _execute_sleep = Float(0.0005, config=True)
105
105
106 # Frequency of the kernel's event loop.
106 # Frequency of the kernel's event loop.
107 # Units are in seconds, kernel subclasses for GUI toolkits may need to
107 # Units are in seconds, kernel subclasses for GUI toolkits may need to
108 # adapt to milliseconds.
108 # adapt to milliseconds.
109 _poll_interval = Float(0.05, config=True)
109 _poll_interval = Float(0.05, config=True)
110
110
111 # If the shutdown was requested over the network, we leave here the
111 # If the shutdown was requested over the network, we leave here the
112 # necessary reply message so it can be sent by our registered atexit
112 # necessary reply message so it can be sent by our registered atexit
113 # handler. This ensures that the reply is only sent to clients truly at
113 # handler. This ensures that the reply is only sent to clients truly at
114 # the end of our shutdown process (which happens after the underlying
114 # the end of our shutdown process (which happens after the underlying
115 # IPython shell's own shutdown).
115 # IPython shell's own shutdown).
116 _shutdown_message = None
116 _shutdown_message = None
117
117
118 # This is a dict of port number that the kernel is listening on. It is set
118 # This is a dict of port number that the kernel is listening on. It is set
119 # by record_ports and used by connect_request.
119 # by record_ports and used by connect_request.
120 _recorded_ports = Dict()
120 _recorded_ports = Dict()
121
121
122 # A reference to the Python builtin 'raw_input' function.
122 # A reference to the Python builtin 'raw_input' function.
123 # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
123 # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
124 _sys_raw_input = Any()
124 _sys_raw_input = Any()
125 _sys_eval_input = Any()
125 _sys_eval_input = Any()
126
126
127 # set of aborted msg_ids
127 # set of aborted msg_ids
128 aborted = Set()
128 aborted = Set()
129
129
130
130
131 def __init__(self, **kwargs):
131 def __init__(self, **kwargs):
132 super(Kernel, self).__init__(**kwargs)
132 super(Kernel, self).__init__(**kwargs)
133
133
134 # Initialize the InteractiveShell subclass
134 # Initialize the InteractiveShell subclass
135 self.shell = self.shell_class.instance(parent=self,
135 self.shell = self.shell_class.instance(parent=self,
136 profile_dir = self.profile_dir,
136 profile_dir = self.profile_dir,
137 user_module = self.user_module,
137 user_module = self.user_module,
138 user_ns = self.user_ns,
138 user_ns = self.user_ns,
139 kernel = self,
139 kernel = self,
140 )
140 )
141 self.shell.displayhook.session = self.session
141 self.shell.displayhook.session = self.session
142 self.shell.displayhook.pub_socket = self.iopub_socket
142 self.shell.displayhook.pub_socket = self.iopub_socket
143 self.shell.displayhook.topic = self._topic('pyout')
143 self.shell.displayhook.topic = self._topic('pyout')
144 self.shell.display_pub.session = self.session
144 self.shell.display_pub.session = self.session
145 self.shell.display_pub.pub_socket = self.iopub_socket
145 self.shell.display_pub.pub_socket = self.iopub_socket
146 self.shell.data_pub.session = self.session
146 self.shell.data_pub.session = self.session
147 self.shell.data_pub.pub_socket = self.iopub_socket
147 self.shell.data_pub.pub_socket = self.iopub_socket
148
148
149 # TMP - hack while developing
149 # TMP - hack while developing
150 self.shell._reply_content = None
150 self.shell._reply_content = None
151
151
152 # Build dict of handlers for message types
152 # Build dict of handlers for message types
153 msg_types = [ 'execute_request', 'complete_request',
153 msg_types = [ 'execute_request', 'complete_request',
154 'object_info_request', 'history_request',
154 'object_info_request', 'history_request',
155 'kernel_info_request',
155 'kernel_info_request',
156 'connect_request', 'shutdown_request',
156 'connect_request', 'shutdown_request',
157 'apply_request',
157 'apply_request',
158 ]
158 ]
159 self.shell_handlers = {}
159 self.shell_handlers = {}
160 for msg_type in msg_types:
160 for msg_type in msg_types:
161 self.shell_handlers[msg_type] = getattr(self, msg_type)
161 self.shell_handlers[msg_type] = getattr(self, msg_type)
162
162
163 comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
163 comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
164 comm_manager = self.shell.comm_manager
164 comm_manager = self.shell.comm_manager
165 for msg_type in comm_msg_types:
165 for msg_type in comm_msg_types:
166 self.shell_handlers[msg_type] = getattr(comm_manager, msg_type)
166 self.shell_handlers[msg_type] = getattr(comm_manager, msg_type)
167
167
168 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
168 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
169 self.control_handlers = {}
169 self.control_handlers = {}
170 for msg_type in control_msg_types:
170 for msg_type in control_msg_types:
171 self.control_handlers[msg_type] = getattr(self, msg_type)
171 self.control_handlers[msg_type] = getattr(self, msg_type)
172
172
173
173
174 def dispatch_control(self, msg):
174 def dispatch_control(self, msg):
175 """dispatch control requests"""
175 """dispatch control requests"""
176 idents,msg = self.session.feed_identities(msg, copy=False)
176 idents,msg = self.session.feed_identities(msg, copy=False)
177 try:
177 try:
178 msg = self.session.unserialize(msg, content=True, copy=False)
178 msg = self.session.unserialize(msg, content=True, copy=False)
179 except:
179 except:
180 self.log.error("Invalid Control Message", exc_info=True)
180 self.log.error("Invalid Control Message", exc_info=True)
181 return
181 return
182
182
183 self.log.debug("Control received: %s", msg)
183 self.log.debug("Control received: %s", msg)
184
184
185 header = msg['header']
185 header = msg['header']
186 msg_id = header['msg_id']
186 msg_id = header['msg_id']
187 msg_type = header['msg_type']
187 msg_type = header['msg_type']
188
188
189 handler = self.control_handlers.get(msg_type, None)
189 handler = self.control_handlers.get(msg_type, None)
190 if handler is None:
190 if handler is None:
191 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
191 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
192 else:
192 else:
193 try:
193 try:
194 handler(self.control_stream, idents, msg)
194 handler(self.control_stream, idents, msg)
195 except Exception:
195 except Exception:
196 self.log.error("Exception in control handler:", exc_info=True)
196 self.log.error("Exception in control handler:", exc_info=True)
197
197
198 def dispatch_shell(self, stream, msg):
198 def dispatch_shell(self, stream, msg):
199 """dispatch shell requests"""
199 """dispatch shell requests"""
200 # flush control requests first
200 # flush control requests first
201 if self.control_stream:
201 if self.control_stream:
202 self.control_stream.flush()
202 self.control_stream.flush()
203
203
204 idents,msg = self.session.feed_identities(msg, copy=False)
204 idents,msg = self.session.feed_identities(msg, copy=False)
205 try:
205 try:
206 msg = self.session.unserialize(msg, content=True, copy=False)
206 msg = self.session.unserialize(msg, content=True, copy=False)
207 except:
207 except:
208 self.log.error("Invalid Message", exc_info=True)
208 self.log.error("Invalid Message", exc_info=True)
209 return
209 return
210
210
211 header = msg['header']
211 header = msg['header']
212 msg_id = header['msg_id']
212 msg_id = header['msg_id']
213 msg_type = msg['header']['msg_type']
213 msg_type = msg['header']['msg_type']
214
214
215 # Print some info about this message and leave a '--->' marker, so it's
215 # Print some info about this message and leave a '--->' marker, so it's
216 # easier to trace visually the message chain when debugging. Each
216 # easier to trace visually the message chain when debugging. Each
217 # handler prints its message at the end.
217 # handler prints its message at the end.
218 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
218 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
219 self.log.debug(' Content: %s\n --->\n ', msg['content'])
219 self.log.debug(' Content: %s\n --->\n ', msg['content'])
220
220
221 if msg_id in self.aborted:
221 if msg_id in self.aborted:
222 self.aborted.remove(msg_id)
222 self.aborted.remove(msg_id)
223 # is it safe to assume a msg_id will not be resubmitted?
223 # is it safe to assume a msg_id will not be resubmitted?
224 reply_type = msg_type.split('_')[0] + '_reply'
224 reply_type = msg_type.split('_')[0] + '_reply'
225 status = {'status' : 'aborted'}
225 status = {'status' : 'aborted'}
226 md = {'engine' : self.ident}
226 md = {'engine' : self.ident}
227 md.update(status)
227 md.update(status)
228 reply_msg = self.session.send(stream, reply_type, metadata=md,
228 reply_msg = self.session.send(stream, reply_type, metadata=md,
229 content=status, parent=msg, ident=idents)
229 content=status, parent=msg, ident=idents)
230 return
230 return
231
231
232 handler = self.shell_handlers.get(msg_type, None)
232 handler = self.shell_handlers.get(msg_type, None)
233 if handler is None:
233 if handler is None:
234 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
234 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
235 else:
235 else:
236 # ensure default_int_handler during handler call
236 # ensure default_int_handler during handler call
237 sig = signal(SIGINT, default_int_handler)
237 sig = signal(SIGINT, default_int_handler)
238 try:
238 try:
239 handler(stream, idents, msg)
239 handler(stream, idents, msg)
240 except Exception:
240 except Exception:
241 self.log.error("Exception in message handler:", exc_info=True)
241 self.log.error("Exception in message handler:", exc_info=True)
242 finally:
242 finally:
243 signal(SIGINT, sig)
243 signal(SIGINT, sig)
244
244
245 def enter_eventloop(self):
245 def enter_eventloop(self):
246 """enter eventloop"""
246 """enter eventloop"""
247 self.log.info("entering eventloop")
247 self.log.info("entering eventloop")
248 # restore default_int_handler
248 # restore default_int_handler
249 signal(SIGINT, default_int_handler)
249 signal(SIGINT, default_int_handler)
250 while self.eventloop is not None:
250 while self.eventloop is not None:
251 try:
251 try:
252 self.eventloop(self)
252 self.eventloop(self)
253 except KeyboardInterrupt:
253 except KeyboardInterrupt:
254 # Ctrl-C shouldn't crash the kernel
254 # Ctrl-C shouldn't crash the kernel
255 self.log.error("KeyboardInterrupt caught in kernel")
255 self.log.error("KeyboardInterrupt caught in kernel")
256 continue
256 continue
257 else:
257 else:
258 # eventloop exited cleanly, this means we should stop (right?)
258 # eventloop exited cleanly, this means we should stop (right?)
259 self.eventloop = None
259 self.eventloop = None
260 break
260 break
261 self.log.info("exiting eventloop")
261 self.log.info("exiting eventloop")
262
262
263 def start(self):
263 def start(self):
264 """register dispatchers for streams"""
264 """register dispatchers for streams"""
265 self.shell.exit_now = False
265 self.shell.exit_now = False
266 if self.control_stream:
266 if self.control_stream:
267 self.control_stream.on_recv(self.dispatch_control, copy=False)
267 self.control_stream.on_recv(self.dispatch_control, copy=False)
268
268
269 def make_dispatcher(stream):
269 def make_dispatcher(stream):
270 def dispatcher(msg):
270 def dispatcher(msg):
271 return self.dispatch_shell(stream, msg)
271 return self.dispatch_shell(stream, msg)
272 return dispatcher
272 return dispatcher
273
273
274 for s in self.shell_streams:
274 for s in self.shell_streams:
275 s.on_recv(make_dispatcher(s), copy=False)
275 s.on_recv(make_dispatcher(s), copy=False)
276
276
277 # publish idle status
277 # publish idle status
278 self._publish_status('starting')
278 self._publish_status('starting')
279
279
280 def do_one_iteration(self):
280 def do_one_iteration(self):
281 """step eventloop just once"""
281 """step eventloop just once"""
282 if self.control_stream:
282 if self.control_stream:
283 self.control_stream.flush()
283 self.control_stream.flush()
284 for stream in self.shell_streams:
284 for stream in self.shell_streams:
285 # handle at most one request per iteration
285 # handle at most one request per iteration
286 stream.flush(zmq.POLLIN, 1)
286 stream.flush(zmq.POLLIN, 1)
287 stream.flush(zmq.POLLOUT)
287 stream.flush(zmq.POLLOUT)
288
288
289
289
290 def record_ports(self, ports):
290 def record_ports(self, ports):
291 """Record the ports that this kernel is using.
291 """Record the ports that this kernel is using.
292
292
293 The creator of the Kernel instance must call this methods if they
293 The creator of the Kernel instance must call this methods if they
294 want the :meth:`connect_request` method to return the port numbers.
294 want the :meth:`connect_request` method to return the port numbers.
295 """
295 """
296 self._recorded_ports = ports
296 self._recorded_ports = ports
297
297
298 #---------------------------------------------------------------------------
298 #---------------------------------------------------------------------------
299 # Kernel request handlers
299 # Kernel request handlers
300 #---------------------------------------------------------------------------
300 #---------------------------------------------------------------------------
301
301
302 def _make_metadata(self, other=None):
302 def _make_metadata(self, other=None):
303 """init metadata dict, for execute/apply_reply"""
303 """init metadata dict, for execute/apply_reply"""
304 new_md = {
304 new_md = {
305 'dependencies_met' : True,
305 'dependencies_met' : True,
306 'engine' : self.ident,
306 'engine' : self.ident,
307 'started': datetime.now(),
307 'started': datetime.now(),
308 }
308 }
309 if other:
309 if other:
310 new_md.update(other)
310 new_md.update(other)
311 return new_md
311 return new_md
312
312
313 def _publish_pyin(self, code, parent, execution_count):
313 def _publish_pyin(self, code, parent, execution_count):
314 """Publish the code request on the pyin stream."""
314 """Publish the code request on the pyin stream."""
315
315
316 self.session.send(self.iopub_socket, u'pyin',
316 self.session.send(self.iopub_socket, u'pyin',
317 {u'code':code, u'execution_count': execution_count},
317 {u'code':code, u'execution_count': execution_count},
318 parent=parent, ident=self._topic('pyin')
318 parent=parent, ident=self._topic('pyin')
319 )
319 )
320
320
321 def _publish_status(self, status, parent=None):
321 def _publish_status(self, status, parent=None):
322 """send status (busy/idle) on IOPub"""
322 """send status (busy/idle) on IOPub"""
323 self.session.send(self.iopub_socket,
323 self.session.send(self.iopub_socket,
324 u'status',
324 u'status',
325 {u'execution_state': status},
325 {u'execution_state': status},
326 parent=parent,
326 parent=parent,
327 ident=self._topic('status'),
327 ident=self._topic('status'),
328 )
328 )
329
329
330
330
331 def execute_request(self, stream, ident, parent):
331 def execute_request(self, stream, ident, parent):
332 """handle an execute_request"""
332 """handle an execute_request"""
333
333
334 self._publish_status(u'busy', parent)
334 self._publish_status(u'busy', parent)
335
335
336 try:
336 try:
337 content = parent[u'content']
337 content = parent[u'content']
338 code = content[u'code']
338 code = content[u'code']
339 silent = content[u'silent']
339 silent = content[u'silent']
340 store_history = content.get(u'store_history', not silent)
340 store_history = content.get(u'store_history', not silent)
341 except:
341 except:
342 self.log.error("Got bad msg: ")
342 self.log.error("Got bad msg: ")
343 self.log.error("%s", parent)
343 self.log.error("%s", parent)
344 return
344 return
345
345
346 md = self._make_metadata(parent['metadata'])
346 md = self._make_metadata(parent['metadata'])
347
347
348 shell = self.shell # we'll need this a lot here
348 shell = self.shell # we'll need this a lot here
349
349
350 # Replace raw_input. Note that is not sufficient to replace
350 # Replace raw_input. Note that is not sufficient to replace
351 # raw_input in the user namespace.
351 # raw_input in the user namespace.
352 if content.get('allow_stdin', False):
352 if content.get('allow_stdin', False):
353 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
353 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
354 input = lambda prompt='': eval(raw_input(prompt))
354 input = lambda prompt='': eval(raw_input(prompt))
355 else:
355 else:
356 raw_input = input = lambda prompt='' : self._no_raw_input()
356 raw_input = input = lambda prompt='' : self._no_raw_input()
357
357
358 if py3compat.PY3:
358 if py3compat.PY3:
359 self._sys_raw_input = __builtin__.input
359 self._sys_raw_input = __builtin__.input
360 __builtin__.input = raw_input
360 __builtin__.input = raw_input
361 else:
361 else:
362 self._sys_raw_input = __builtin__.raw_input
362 self._sys_raw_input = __builtin__.raw_input
363 self._sys_eval_input = __builtin__.input
363 self._sys_eval_input = __builtin__.input
364 __builtin__.raw_input = raw_input
364 __builtin__.raw_input = raw_input
365 __builtin__.input = input
365 __builtin__.input = input
366
366
367 # Set the parent message of the display hook and out streams.
367 # Set the parent message of the display hook and out streams.
368 shell.set_parent(parent)
368 shell.set_parent(parent)
369
369
370 # Re-broadcast our input for the benefit of listening clients, and
370 # Re-broadcast our input for the benefit of listening clients, and
371 # start computing output
371 # start computing output
372 if not silent:
372 if not silent:
373 self._publish_pyin(code, parent, shell.execution_count)
373 self._publish_pyin(code, parent, shell.execution_count)
374
374
375 reply_content = {}
375 reply_content = {}
376 try:
376 try:
377 # FIXME: the shell calls the exception handler itself.
377 # FIXME: the shell calls the exception handler itself.
378 shell.run_cell(code, store_history=store_history, silent=silent)
378 shell.run_cell(code, store_history=store_history, silent=silent)
379 except:
379 except:
380 status = u'error'
380 status = u'error'
381 # FIXME: this code right now isn't being used yet by default,
381 # FIXME: this code right now isn't being used yet by default,
382 # because the run_cell() call above directly fires off exception
382 # because the run_cell() call above directly fires off exception
383 # reporting. This code, therefore, is only active in the scenario
383 # reporting. This code, therefore, is only active in the scenario
384 # where runlines itself has an unhandled exception. We need to
384 # where runlines itself has an unhandled exception. We need to
385 # uniformize this, for all exception construction to come from a
385 # uniformize this, for all exception construction to come from a
386 # single location in the codbase.
386 # single location in the codbase.
387 etype, evalue, tb = sys.exc_info()
387 etype, evalue, tb = sys.exc_info()
388 tb_list = traceback.format_exception(etype, evalue, tb)
388 tb_list = traceback.format_exception(etype, evalue, tb)
389 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
389 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
390 else:
390 else:
391 status = u'ok'
391 status = u'ok'
392 finally:
392 finally:
393 # Restore raw_input.
393 # Restore raw_input.
394 if py3compat.PY3:
394 if py3compat.PY3:
395 __builtin__.input = self._sys_raw_input
395 __builtin__.input = self._sys_raw_input
396 else:
396 else:
397 __builtin__.raw_input = self._sys_raw_input
397 __builtin__.raw_input = self._sys_raw_input
398 __builtin__.input = self._sys_eval_input
398 __builtin__.input = self._sys_eval_input
399
399
400 reply_content[u'status'] = status
400 reply_content[u'status'] = status
401
401
402 # Return the execution counter so clients can display prompts
402 # Return the execution counter so clients can display prompts
403 reply_content['execution_count'] = shell.execution_count - 1
403 reply_content['execution_count'] = shell.execution_count - 1
404
404
405 # FIXME - fish exception info out of shell, possibly left there by
405 # FIXME - fish exception info out of shell, possibly left there by
406 # runlines. We'll need to clean up this logic later.
406 # runlines. We'll need to clean up this logic later.
407 if shell._reply_content is not None:
407 if shell._reply_content is not None:
408 reply_content.update(shell._reply_content)
408 reply_content.update(shell._reply_content)
409 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
409 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
410 reply_content['engine_info'] = e_info
410 reply_content['engine_info'] = e_info
411 # reset after use
411 # reset after use
412 shell._reply_content = None
412 shell._reply_content = None
413
413
414 if 'traceback' in reply_content:
414 if 'traceback' in reply_content:
415 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
415 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
416
416
417
417
418 # At this point, we can tell whether the main code execution succeeded
418 # At this point, we can tell whether the main code execution succeeded
419 # or not. If it did, we proceed to evaluate user_variables/expressions
419 # or not. If it did, we proceed to evaluate user_variables/expressions
420 if reply_content['status'] == 'ok':
420 if reply_content['status'] == 'ok':
421 reply_content[u'user_variables'] = \
421 reply_content[u'user_variables'] = \
422 shell.user_variables(content.get(u'user_variables', []))
422 shell.user_variables(content.get(u'user_variables', []))
423 reply_content[u'user_expressions'] = \
423 reply_content[u'user_expressions'] = \
424 shell.user_expressions(content.get(u'user_expressions', {}))
424 shell.user_expressions(content.get(u'user_expressions', {}))
425 else:
425 else:
426 # If there was an error, don't even try to compute variables or
426 # If there was an error, don't even try to compute variables or
427 # expressions
427 # expressions
428 reply_content[u'user_variables'] = {}
428 reply_content[u'user_variables'] = {}
429 reply_content[u'user_expressions'] = {}
429 reply_content[u'user_expressions'] = {}
430
430
431 # Payloads should be retrieved regardless of outcome, so we can both
431 # Payloads should be retrieved regardless of outcome, so we can both
432 # recover partial output (that could have been generated early in a
432 # recover partial output (that could have been generated early in a
433 # block, before an error) and clear the payload system always.
433 # block, before an error) and clear the payload system always.
434 reply_content[u'payload'] = shell.payload_manager.read_payload()
434 reply_content[u'payload'] = shell.payload_manager.read_payload()
435 # Be agressive about clearing the payload because we don't want
435 # Be agressive about clearing the payload because we don't want
436 # it to sit in memory until the next execute_request comes in.
436 # it to sit in memory until the next execute_request comes in.
437 shell.payload_manager.clear_payload()
437 shell.payload_manager.clear_payload()
438
438
439 # Flush output before sending the reply.
439 # Flush output before sending the reply.
440 sys.stdout.flush()
440 sys.stdout.flush()
441 sys.stderr.flush()
441 sys.stderr.flush()
442 # FIXME: on rare occasions, the flush doesn't seem to make it to the
442 # FIXME: on rare occasions, the flush doesn't seem to make it to the
443 # clients... This seems to mitigate the problem, but we definitely need
443 # clients... This seems to mitigate the problem, but we definitely need
444 # to better understand what's going on.
444 # to better understand what's going on.
445 if self._execute_sleep:
445 if self._execute_sleep:
446 time.sleep(self._execute_sleep)
446 time.sleep(self._execute_sleep)
447
447
448 # Send the reply.
448 # Send the reply.
449 reply_content = json_clean(reply_content)
449 reply_content = json_clean(reply_content)
450
450
451 md['status'] = reply_content['status']
451 md['status'] = reply_content['status']
452 if reply_content['status'] == 'error' and \
452 if reply_content['status'] == 'error' and \
453 reply_content['ename'] == 'UnmetDependency':
453 reply_content['ename'] == 'UnmetDependency':
454 md['dependencies_met'] = False
454 md['dependencies_met'] = False
455
455
456 reply_msg = self.session.send(stream, u'execute_reply',
456 reply_msg = self.session.send(stream, u'execute_reply',
457 reply_content, parent, metadata=md,
457 reply_content, parent, metadata=md,
458 ident=ident)
458 ident=ident)
459
459
460 self.log.debug("%s", reply_msg)
460 self.log.debug("%s", reply_msg)
461
461
462 if not silent and reply_msg['content']['status'] == u'error':
462 if not silent and reply_msg['content']['status'] == u'error':
463 self._abort_queues()
463 self._abort_queues()
464
464
465 self._publish_status(u'idle', parent)
465 self._publish_status(u'idle', parent)
466
466
467 def complete_request(self, stream, ident, parent):
467 def complete_request(self, stream, ident, parent):
468 txt, matches = self._complete(parent)
468 txt, matches = self._complete(parent)
469 matches = {'matches' : matches,
469 matches = {'matches' : matches,
470 'matched_text' : txt,
470 'matched_text' : txt,
471 'status' : 'ok'}
471 'status' : 'ok'}
472 matches = json_clean(matches)
472 matches = json_clean(matches)
473 completion_msg = self.session.send(stream, 'complete_reply',
473 completion_msg = self.session.send(stream, 'complete_reply',
474 matches, parent, ident)
474 matches, parent, ident)
475 self.log.debug("%s", completion_msg)
475 self.log.debug("%s", completion_msg)
476
476
477 def object_info_request(self, stream, ident, parent):
477 def object_info_request(self, stream, ident, parent):
478 content = parent['content']
478 content = parent['content']
479 object_info = self.shell.object_inspect(content['oname'],
479 object_info = self.shell.object_inspect(content['oname'],
480 detail_level = content.get('detail_level', 0)
480 detail_level = content.get('detail_level', 0)
481 )
481 )
482 # Before we send this object over, we scrub it for JSON usage
482 # Before we send this object over, we scrub it for JSON usage
483 oinfo = json_clean(object_info)
483 oinfo = json_clean(object_info)
484 msg = self.session.send(stream, 'object_info_reply',
484 msg = self.session.send(stream, 'object_info_reply',
485 oinfo, parent, ident)
485 oinfo, parent, ident)
486 self.log.debug("%s", msg)
486 self.log.debug("%s", msg)
487
487
488 def history_request(self, stream, ident, parent):
488 def history_request(self, stream, ident, parent):
489 # We need to pull these out, as passing **kwargs doesn't work with
489 # We need to pull these out, as passing **kwargs doesn't work with
490 # unicode keys before Python 2.6.5.
490 # unicode keys before Python 2.6.5.
491 hist_access_type = parent['content']['hist_access_type']
491 hist_access_type = parent['content']['hist_access_type']
492 raw = parent['content']['raw']
492 raw = parent['content']['raw']
493 output = parent['content']['output']
493 output = parent['content']['output']
494 if hist_access_type == 'tail':
494 if hist_access_type == 'tail':
495 n = parent['content']['n']
495 n = parent['content']['n']
496 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
496 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
497 include_latest=True)
497 include_latest=True)
498
498
499 elif hist_access_type == 'range':
499 elif hist_access_type == 'range':
500 session = parent['content']['session']
500 session = parent['content']['session']
501 start = parent['content']['start']
501 start = parent['content']['start']
502 stop = parent['content']['stop']
502 stop = parent['content']['stop']
503 hist = self.shell.history_manager.get_range(session, start, stop,
503 hist = self.shell.history_manager.get_range(session, start, stop,
504 raw=raw, output=output)
504 raw=raw, output=output)
505
505
506 elif hist_access_type == 'search':
506 elif hist_access_type == 'search':
507 n = parent['content'].get('n')
507 n = parent['content'].get('n')
508 unique = parent['content'].get('unique', False)
508 unique = parent['content'].get('unique', False)
509 pattern = parent['content']['pattern']
509 pattern = parent['content']['pattern']
510 hist = self.shell.history_manager.search(
510 hist = self.shell.history_manager.search(
511 pattern, raw=raw, output=output, n=n, unique=unique)
511 pattern, raw=raw, output=output, n=n, unique=unique)
512
512
513 else:
513 else:
514 hist = []
514 hist = []
515 hist = list(hist)
515 hist = list(hist)
516 content = {'history' : hist}
516 content = {'history' : hist}
517 content = json_clean(content)
517 content = json_clean(content)
518 msg = self.session.send(stream, 'history_reply',
518 msg = self.session.send(stream, 'history_reply',
519 content, parent, ident)
519 content, parent, ident)
520 self.log.debug("Sending history reply with %i entries", len(hist))
520 self.log.debug("Sending history reply with %i entries", len(hist))
521
521
522 def connect_request(self, stream, ident, parent):
522 def connect_request(self, stream, ident, parent):
523 if self._recorded_ports is not None:
523 if self._recorded_ports is not None:
524 content = self._recorded_ports.copy()
524 content = self._recorded_ports.copy()
525 else:
525 else:
526 content = {}
526 content = {}
527 msg = self.session.send(stream, 'connect_reply',
527 msg = self.session.send(stream, 'connect_reply',
528 content, parent, ident)
528 content, parent, ident)
529 self.log.debug("%s", msg)
529 self.log.debug("%s", msg)
530
530
531 def kernel_info_request(self, stream, ident, parent):
531 def kernel_info_request(self, stream, ident, parent):
532 vinfo = {
532 vinfo = {
533 'protocol_version': protocol_version,
533 'protocol_version': protocol_version,
534 'ipython_version': ipython_version,
534 'ipython_version': ipython_version,
535 'language_version': language_version,
535 'language_version': language_version,
536 'language': 'python',
536 'language': 'python',
537 }
537 }
538 msg = self.session.send(stream, 'kernel_info_reply',
538 msg = self.session.send(stream, 'kernel_info_reply',
539 vinfo, parent, ident)
539 vinfo, parent, ident)
540 self.log.debug("%s", msg)
540 self.log.debug("%s", msg)
541
541
542 def shutdown_request(self, stream, ident, parent):
542 def shutdown_request(self, stream, ident, parent):
543 self.shell.exit_now = True
543 self.shell.exit_now = True
544 content = dict(status='ok')
544 content = dict(status='ok')
545 content.update(parent['content'])
545 content.update(parent['content'])
546 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
546 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
547 # same content, but different msg_id for broadcasting on IOPub
547 # same content, but different msg_id for broadcasting on IOPub
548 self._shutdown_message = self.session.msg(u'shutdown_reply',
548 self._shutdown_message = self.session.msg(u'shutdown_reply',
549 content, parent
549 content, parent
550 )
550 )
551
551
552 self._at_shutdown()
552 self._at_shutdown()
553 # call sys.exit after a short delay
553 # call sys.exit after a short delay
554 loop = ioloop.IOLoop.instance()
554 loop = ioloop.IOLoop.instance()
555 loop.add_timeout(time.time()+0.1, loop.stop)
555 loop.add_timeout(time.time()+0.1, loop.stop)
556
556
557 #---------------------------------------------------------------------------
557 #---------------------------------------------------------------------------
558 # Engine methods
558 # Engine methods
559 #---------------------------------------------------------------------------
559 #---------------------------------------------------------------------------
560
560
561 def apply_request(self, stream, ident, parent):
561 def apply_request(self, stream, ident, parent):
562 try:
562 try:
563 content = parent[u'content']
563 content = parent[u'content']
564 bufs = parent[u'buffers']
564 bufs = parent[u'buffers']
565 msg_id = parent['header']['msg_id']
565 msg_id = parent['header']['msg_id']
566 except:
566 except:
567 self.log.error("Got bad msg: %s", parent, exc_info=True)
567 self.log.error("Got bad msg: %s", parent, exc_info=True)
568 return
568 return
569
569
570 self._publish_status(u'busy', parent)
570 self._publish_status(u'busy', parent)
571
571
572 # Set the parent message of the display hook and out streams.
572 # Set the parent message of the display hook and out streams.
573 shell = self.shell
573 shell = self.shell
574 shell.set_parent(parent)
574 shell.set_parent(parent)
575
575
576 # pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
576 # pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
577 # self.iopub_socket.send(pyin_msg)
577 # self.iopub_socket.send(pyin_msg)
578 # self.session.send(self.iopub_socket, u'pyin', {u'code':code},parent=parent)
578 # self.session.send(self.iopub_socket, u'pyin', {u'code':code},parent=parent)
579 md = self._make_metadata(parent['metadata'])
579 md = self._make_metadata(parent['metadata'])
580 try:
580 try:
581 working = shell.user_ns
581 working = shell.user_ns
582
582
583 prefix = "_"+str(msg_id).replace("-","")+"_"
583 prefix = "_"+str(msg_id).replace("-","")+"_"
584
584
585 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
585 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
586
586
587 fname = getattr(f, '__name__', 'f')
587 fname = getattr(f, '__name__', 'f')
588
588
589 fname = prefix+"f"
589 fname = prefix+"f"
590 argname = prefix+"args"
590 argname = prefix+"args"
591 kwargname = prefix+"kwargs"
591 kwargname = prefix+"kwargs"
592 resultname = prefix+"result"
592 resultname = prefix+"result"
593
593
594 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
594 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
595 # print ns
595 # print ns
596 working.update(ns)
596 working.update(ns)
597 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
597 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
598 try:
598 try:
599 exec code in shell.user_global_ns, shell.user_ns
599 exec code in shell.user_global_ns, shell.user_ns
600 result = working.get(resultname)
600 result = working.get(resultname)
601 finally:
601 finally:
602 for key in ns.iterkeys():
602 for key in ns.iterkeys():
603 working.pop(key)
603 working.pop(key)
604
604
605 result_buf = serialize_object(result,
605 result_buf = serialize_object(result,
606 buffer_threshold=self.session.buffer_threshold,
606 buffer_threshold=self.session.buffer_threshold,
607 item_threshold=self.session.item_threshold,
607 item_threshold=self.session.item_threshold,
608 )
608 )
609
609
610 except:
610 except:
611 # invoke IPython traceback formatting
611 # invoke IPython traceback formatting
612 shell.showtraceback()
612 shell.showtraceback()
613 # FIXME - fish exception info out of shell, possibly left there by
613 # FIXME - fish exception info out of shell, possibly left there by
614 # run_code. We'll need to clean up this logic later.
614 # run_code. We'll need to clean up this logic later.
615 reply_content = {}
615 reply_content = {}
616 if shell._reply_content is not None:
616 if shell._reply_content is not None:
617 reply_content.update(shell._reply_content)
617 reply_content.update(shell._reply_content)
618 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
618 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
619 reply_content['engine_info'] = e_info
619 reply_content['engine_info'] = e_info
620 # reset after use
620 # reset after use
621 shell._reply_content = None
621 shell._reply_content = None
622
622
623 self.session.send(self.iopub_socket, u'pyerr', reply_content, parent=parent,
623 self.session.send(self.iopub_socket, u'pyerr', reply_content, parent=parent,
624 ident=self._topic('pyerr'))
624 ident=self._topic('pyerr'))
625 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
625 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
626 result_buf = []
626 result_buf = []
627
627
628 if reply_content['ename'] == 'UnmetDependency':
628 if reply_content['ename'] == 'UnmetDependency':
629 md['dependencies_met'] = False
629 md['dependencies_met'] = False
630 else:
630 else:
631 reply_content = {'status' : 'ok'}
631 reply_content = {'status' : 'ok'}
632
632
633 # put 'ok'/'error' status in header, for scheduler introspection:
633 # put 'ok'/'error' status in header, for scheduler introspection:
634 md['status'] = reply_content['status']
634 md['status'] = reply_content['status']
635
635
636 # flush i/o
636 # flush i/o
637 sys.stdout.flush()
637 sys.stdout.flush()
638 sys.stderr.flush()
638 sys.stderr.flush()
639
639
640 reply_msg = self.session.send(stream, u'apply_reply', reply_content,
640 reply_msg = self.session.send(stream, u'apply_reply', reply_content,
641 parent=parent, ident=ident,buffers=result_buf, metadata=md)
641 parent=parent, ident=ident,buffers=result_buf, metadata=md)
642
642
643 self._publish_status(u'idle', parent)
643 self._publish_status(u'idle', parent)
644
644
645 #---------------------------------------------------------------------------
645 #---------------------------------------------------------------------------
646 # Control messages
646 # Control messages
647 #---------------------------------------------------------------------------
647 #---------------------------------------------------------------------------
648
648
649 def abort_request(self, stream, ident, parent):
649 def abort_request(self, stream, ident, parent):
650 """abort a specifig msg by id"""
650 """abort a specifig msg by id"""
651 msg_ids = parent['content'].get('msg_ids', None)
651 msg_ids = parent['content'].get('msg_ids', None)
652 if isinstance(msg_ids, basestring):
652 if isinstance(msg_ids, basestring):
653 msg_ids = [msg_ids]
653 msg_ids = [msg_ids]
654 if not msg_ids:
654 if not msg_ids:
655 self.abort_queues()
655 self.abort_queues()
656 for mid in msg_ids:
656 for mid in msg_ids:
657 self.aborted.add(str(mid))
657 self.aborted.add(str(mid))
658
658
659 content = dict(status='ok')
659 content = dict(status='ok')
660 reply_msg = self.session.send(stream, 'abort_reply', content=content,
660 reply_msg = self.session.send(stream, 'abort_reply', content=content,
661 parent=parent, ident=ident)
661 parent=parent, ident=ident)
662 self.log.debug("%s", reply_msg)
662 self.log.debug("%s", reply_msg)
663
663
664 def clear_request(self, stream, idents, parent):
664 def clear_request(self, stream, idents, parent):
665 """Clear our namespace."""
665 """Clear our namespace."""
666 self.shell.reset(False)
666 self.shell.reset(False)
667 msg = self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
667 msg = self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
668 content = dict(status='ok'))
668 content = dict(status='ok'))
669
669
670
670
671 #---------------------------------------------------------------------------
671 #---------------------------------------------------------------------------
672 # Protected interface
672 # Protected interface
673 #---------------------------------------------------------------------------
673 #---------------------------------------------------------------------------
674
674
675 def _wrap_exception(self, method=None):
675 def _wrap_exception(self, method=None):
676 # import here, because _wrap_exception is only used in parallel,
676 # import here, because _wrap_exception is only used in parallel,
677 # and parallel has higher min pyzmq version
677 # and parallel has higher min pyzmq version
678 from IPython.parallel.error import wrap_exception
678 from IPython.parallel.error import wrap_exception
679 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method=method)
679 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method=method)
680 content = wrap_exception(e_info)
680 content = wrap_exception(e_info)
681 return content
681 return content
682
682
683 def _topic(self, topic):
683 def _topic(self, topic):
684 """prefixed topic for IOPub messages"""
684 """prefixed topic for IOPub messages"""
685 if self.int_id >= 0:
685 if self.int_id >= 0:
686 base = "engine.%i" % self.int_id
686 base = "engine.%i" % self.int_id
687 else:
687 else:
688 base = "kernel.%s" % self.ident
688 base = "kernel.%s" % self.ident
689
689
690 return py3compat.cast_bytes("%s.%s" % (base, topic))
690 return py3compat.cast_bytes("%s.%s" % (base, topic))
691
691
692 def _abort_queues(self):
692 def _abort_queues(self):
693 for stream in self.shell_streams:
693 for stream in self.shell_streams:
694 if stream:
694 if stream:
695 self._abort_queue(stream)
695 self._abort_queue(stream)
696
696
697 def _abort_queue(self, stream):
697 def _abort_queue(self, stream):
698 poller = zmq.Poller()
698 poller = zmq.Poller()
699 poller.register(stream.socket, zmq.POLLIN)
699 poller.register(stream.socket, zmq.POLLIN)
700 while True:
700 while True:
701 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
701 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
702 if msg is None:
702 if msg is None:
703 return
703 return
704
704
705 self.log.info("Aborting:")
705 self.log.info("Aborting:")
706 self.log.info("%s", msg)
706 self.log.info("%s", msg)
707 msg_type = msg['header']['msg_type']
707 msg_type = msg['header']['msg_type']
708 reply_type = msg_type.split('_')[0] + '_reply'
708 reply_type = msg_type.split('_')[0] + '_reply'
709
709
710 status = {'status' : 'aborted'}
710 status = {'status' : 'aborted'}
711 md = {'engine' : self.ident}
711 md = {'engine' : self.ident}
712 md.update(status)
712 md.update(status)
713 reply_msg = self.session.send(stream, reply_type, metadata=md,
713 reply_msg = self.session.send(stream, reply_type, metadata=md,
714 content=status, parent=msg, ident=idents)
714 content=status, parent=msg, ident=idents)
715 self.log.debug("%s", reply_msg)
715 self.log.debug("%s", reply_msg)
716 # We need to wait a bit for requests to come in. This can probably
716 # We need to wait a bit for requests to come in. This can probably
717 # be set shorter for true asynchronous clients.
717 # be set shorter for true asynchronous clients.
718 poller.poll(50)
718 poller.poll(50)
719
719
720
720
721 def _no_raw_input(self):
721 def _no_raw_input(self):
722 """Raise StdinNotImplentedError if active frontend doesn't support
722 """Raise StdinNotImplentedError if active frontend doesn't support
723 stdin."""
723 stdin."""
724 raise StdinNotImplementedError("raw_input was called, but this "
724 raise StdinNotImplementedError("raw_input was called, but this "
725 "frontend does not support stdin.")
725 "frontend does not support stdin.")
726
726
727 def _raw_input(self, prompt, ident, parent):
727 def _raw_input(self, prompt, ident, parent):
728 # Flush output before making the request.
728 # Flush output before making the request.
729 sys.stderr.flush()
729 sys.stderr.flush()
730 sys.stdout.flush()
730 sys.stdout.flush()
731 # flush the stdin socket, to purge stale replies
731 # flush the stdin socket, to purge stale replies
732 while True:
732 while True:
733 try:
733 try:
734 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
734 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
735 except zmq.ZMQError as e:
735 except zmq.ZMQError as e:
736 if e.errno == zmq.EAGAIN:
736 if e.errno == zmq.EAGAIN:
737 break
737 break
738 else:
738 else:
739 raise
739 raise
740
740
741 # Send the input request.
741 # Send the input request.
742 content = json_clean(dict(prompt=prompt))
742 content = json_clean(dict(prompt=prompt))
743 self.session.send(self.stdin_socket, u'input_request', content, parent,
743 self.session.send(self.stdin_socket, u'input_request', content, parent,
744 ident=ident)
744 ident=ident)
745
745
746 # Await a response.
746 # Await a response.
747 while True:
747 while True:
748 try:
748 try:
749 ident, reply = self.session.recv(self.stdin_socket, 0)
749 ident, reply = self.session.recv(self.stdin_socket, 0)
750 except Exception:
750 except Exception:
751 self.log.warn("Invalid Message:", exc_info=True)
751 self.log.warn("Invalid Message:", exc_info=True)
752 except KeyboardInterrupt:
752 except KeyboardInterrupt:
753 # re-raise KeyboardInterrupt, to truncate traceback
753 # re-raise KeyboardInterrupt, to truncate traceback
754 raise KeyboardInterrupt
754 raise KeyboardInterrupt
755 else:
755 else:
756 break
756 break
757 try:
757 try:
758 value = py3compat.unicode_to_str(reply['content']['value'])
758 value = py3compat.unicode_to_str(reply['content']['value'])
759 except:
759 except:
760 self.log.error("Got bad raw_input reply: ")
760 self.log.error("Got bad raw_input reply: ")
761 self.log.error("%s", parent)
761 self.log.error("%s", parent)
762 value = ''
762 value = ''
763 if value == '\x04':
763 if value == '\x04':
764 # EOF
764 # EOF
765 raise EOFError
765 raise EOFError
766 return value
766 return value
767
767
768 def _complete(self, msg):
768 def _complete(self, msg):
769 c = msg['content']
769 c = msg['content']
770 try:
770 try:
771 cpos = int(c['cursor_pos'])
771 cpos = int(c['cursor_pos'])
772 except:
772 except:
773 # If we don't get something that we can convert to an integer, at
773 # If we don't get something that we can convert to an integer, at
774 # least attempt the completion guessing the cursor is at the end of
774 # least attempt the completion guessing the cursor is at the end of
775 # the text, if there's any, and otherwise of the line
775 # the text, if there's any, and otherwise of the line
776 cpos = len(c['text'])
776 cpos = len(c['text'])
777 if cpos==0:
777 if cpos==0:
778 cpos = len(c['line'])
778 cpos = len(c['line'])
779 return self.shell.complete(c['text'], c['line'], cpos)
779 return self.shell.complete(c['text'], c['line'], cpos)
780
780
781 def _at_shutdown(self):
781 def _at_shutdown(self):
782 """Actions taken at shutdown by the kernel, called by python's atexit.
782 """Actions taken at shutdown by the kernel, called by python's atexit.
783 """
783 """
784 # io.rprint("Kernel at_shutdown") # dbg
784 # io.rprint("Kernel at_shutdown") # dbg
785 if self._shutdown_message is not None:
785 if self._shutdown_message is not None:
786 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
786 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
787 self.log.debug("%s", self._shutdown_message)
787 self.log.debug("%s", self._shutdown_message)
788 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
788 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
789
789
@@ -1,473 +1,473 b''
1 """An Application for launching a kernel
1 """An Application for launching a kernel
2
2
3 Authors
3 Authors
4 -------
4 -------
5 * MinRK
5 * MinRK
6 """
6 """
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2011 The IPython Development Team
8 # Copyright (C) 2011 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING.txt, distributed as part of this software.
11 # the file COPYING.txt, distributed as part of this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 from __future__ import print_function
18 from __future__ import print_function
19
19
20 # Standard library imports
20 # Standard library imports
21 import atexit
21 import atexit
22 import json
22 import json
23 import os
23 import os
24 import sys
24 import sys
25 import signal
25 import signal
26
26
27 # System library imports
27 # System library imports
28 import zmq
28 import zmq
29 from zmq.eventloop import ioloop
29 from zmq.eventloop import ioloop
30 from zmq.eventloop.zmqstream import ZMQStream
30 from zmq.eventloop.zmqstream import ZMQStream
31
31
32 # IPython imports
32 # IPython imports
33 from IPython.core.ultratb import FormattedTB
33 from IPython.core.ultratb import FormattedTB
34 from IPython.core.application import (
34 from IPython.core.application import (
35 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
35 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
36 )
36 )
37 from IPython.core.profiledir import ProfileDir
37 from IPython.core.profiledir import ProfileDir
38 from IPython.core.shellapp import (
38 from IPython.core.shellapp import (
39 InteractiveShellApp, shell_flags, shell_aliases
39 InteractiveShellApp, shell_flags, shell_aliases
40 )
40 )
41 from IPython.utils import io
41 from IPython.utils import io
42 from IPython.utils.localinterfaces import localhost
42 from IPython.utils.localinterfaces import localhost
43 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
44 from IPython.utils.py3compat import str_to_bytes
44 from IPython.utils.py3compat import str_to_bytes
45 from IPython.utils.traitlets import (
45 from IPython.utils.traitlets import (
46 Any, Instance, Dict, Unicode, Integer, Bool, CaselessStrEnum,
46 Any, Instance, Dict, Unicode, Integer, Bool, CaselessStrEnum,
47 DottedObjectName,
47 DottedObjectName,
48 )
48 )
49 from IPython.utils.importstring import import_item
49 from IPython.utils.importstring import import_item
50 from IPython.kernel import write_connection_file
50 from IPython.kernel import write_connection_file
51
51
52 # local imports
52 # local imports
53 from heartbeat import Heartbeat
53 from .heartbeat import Heartbeat
54 from ipkernel import Kernel
54 from .ipkernel import Kernel
55 from parentpoller import ParentPollerUnix, ParentPollerWindows
55 from .parentpoller import ParentPollerUnix, ParentPollerWindows
56 from session import (
56 from .session import (
57 Session, session_flags, session_aliases, default_secure,
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 # Flags and Aliases
62 # Flags and Aliases
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64
64
65 kernel_aliases = dict(base_aliases)
65 kernel_aliases = dict(base_aliases)
66 kernel_aliases.update({
66 kernel_aliases.update({
67 'ip' : 'IPKernelApp.ip',
67 'ip' : 'IPKernelApp.ip',
68 'hb' : 'IPKernelApp.hb_port',
68 'hb' : 'IPKernelApp.hb_port',
69 'shell' : 'IPKernelApp.shell_port',
69 'shell' : 'IPKernelApp.shell_port',
70 'iopub' : 'IPKernelApp.iopub_port',
70 'iopub' : 'IPKernelApp.iopub_port',
71 'stdin' : 'IPKernelApp.stdin_port',
71 'stdin' : 'IPKernelApp.stdin_port',
72 'control' : 'IPKernelApp.control_port',
72 'control' : 'IPKernelApp.control_port',
73 'f' : 'IPKernelApp.connection_file',
73 'f' : 'IPKernelApp.connection_file',
74 'parent': 'IPKernelApp.parent_handle',
74 'parent': 'IPKernelApp.parent_handle',
75 'transport': 'IPKernelApp.transport',
75 'transport': 'IPKernelApp.transport',
76 })
76 })
77 if sys.platform.startswith('win'):
77 if sys.platform.startswith('win'):
78 kernel_aliases['interrupt'] = 'IPKernelApp.interrupt'
78 kernel_aliases['interrupt'] = 'IPKernelApp.interrupt'
79
79
80 kernel_flags = dict(base_flags)
80 kernel_flags = dict(base_flags)
81 kernel_flags.update({
81 kernel_flags.update({
82 'no-stdout' : (
82 'no-stdout' : (
83 {'IPKernelApp' : {'no_stdout' : True}},
83 {'IPKernelApp' : {'no_stdout' : True}},
84 "redirect stdout to the null device"),
84 "redirect stdout to the null device"),
85 'no-stderr' : (
85 'no-stderr' : (
86 {'IPKernelApp' : {'no_stderr' : True}},
86 {'IPKernelApp' : {'no_stderr' : True}},
87 "redirect stderr to the null device"),
87 "redirect stderr to the null device"),
88 'pylab' : (
88 'pylab' : (
89 {'IPKernelApp' : {'pylab' : 'auto'}},
89 {'IPKernelApp' : {'pylab' : 'auto'}},
90 """Pre-load matplotlib and numpy for interactive use with
90 """Pre-load matplotlib and numpy for interactive use with
91 the default matplotlib backend."""),
91 the default matplotlib backend."""),
92 })
92 })
93
93
94 # inherit flags&aliases for any IPython shell apps
94 # inherit flags&aliases for any IPython shell apps
95 kernel_aliases.update(shell_aliases)
95 kernel_aliases.update(shell_aliases)
96 kernel_flags.update(shell_flags)
96 kernel_flags.update(shell_flags)
97
97
98 # inherit flags&aliases for Sessions
98 # inherit flags&aliases for Sessions
99 kernel_aliases.update(session_aliases)
99 kernel_aliases.update(session_aliases)
100 kernel_flags.update(session_flags)
100 kernel_flags.update(session_flags)
101
101
102 _ctrl_c_message = """\
102 _ctrl_c_message = """\
103 NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
103 NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
104
104
105 To exit, you will have to explicitly quit this process, by either sending
105 To exit, you will have to explicitly quit this process, by either sending
106 "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
106 "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
107
107
108 To read more about this, see https://github.com/ipython/ipython/issues/2049
108 To read more about this, see https://github.com/ipython/ipython/issues/2049
109
109
110 """
110 """
111
111
112 #-----------------------------------------------------------------------------
112 #-----------------------------------------------------------------------------
113 # Application class for starting an IPython Kernel
113 # Application class for starting an IPython Kernel
114 #-----------------------------------------------------------------------------
114 #-----------------------------------------------------------------------------
115
115
116 class IPKernelApp(BaseIPythonApplication, InteractiveShellApp):
116 class IPKernelApp(BaseIPythonApplication, InteractiveShellApp):
117 name='ipkernel'
117 name='ipkernel'
118 aliases = Dict(kernel_aliases)
118 aliases = Dict(kernel_aliases)
119 flags = Dict(kernel_flags)
119 flags = Dict(kernel_flags)
120 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
120 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
121 # the kernel class, as an importstring
121 # the kernel class, as an importstring
122 kernel_class = DottedObjectName('IPython.kernel.zmq.ipkernel.Kernel', config=True,
122 kernel_class = DottedObjectName('IPython.kernel.zmq.ipkernel.Kernel', config=True,
123 help="""The Kernel subclass to be used.
123 help="""The Kernel subclass to be used.
124
124
125 This should allow easy re-use of the IPKernelApp entry point
125 This should allow easy re-use of the IPKernelApp entry point
126 to configure and launch kernels other than IPython's own.
126 to configure and launch kernels other than IPython's own.
127 """)
127 """)
128 kernel = Any()
128 kernel = Any()
129 poller = Any() # don't restrict this even though current pollers are all Threads
129 poller = Any() # don't restrict this even though current pollers are all Threads
130 heartbeat = Instance(Heartbeat)
130 heartbeat = Instance(Heartbeat)
131 session = Instance('IPython.kernel.zmq.session.Session')
131 session = Instance('IPython.kernel.zmq.session.Session')
132 ports = Dict()
132 ports = Dict()
133
133
134 # ipkernel doesn't get its own config file
134 # ipkernel doesn't get its own config file
135 def _config_file_name_default(self):
135 def _config_file_name_default(self):
136 return 'ipython_config.py'
136 return 'ipython_config.py'
137
137
138 # inherit config file name from parent:
138 # inherit config file name from parent:
139 parent_appname = Unicode(config=True)
139 parent_appname = Unicode(config=True)
140 def _parent_appname_changed(self, name, old, new):
140 def _parent_appname_changed(self, name, old, new):
141 if self.config_file_specified:
141 if self.config_file_specified:
142 # it was manually specified, ignore
142 # it was manually specified, ignore
143 return
143 return
144 self.config_file_name = new.replace('-','_') + u'_config.py'
144 self.config_file_name = new.replace('-','_') + u'_config.py'
145 # don't let this count as specifying the config file
145 # don't let this count as specifying the config file
146 self.config_file_specified.remove(self.config_file_name)
146 self.config_file_specified.remove(self.config_file_name)
147
147
148 # connection info:
148 # connection info:
149 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
149 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
150 ip = Unicode(config=True,
150 ip = Unicode(config=True,
151 help="Set the IP or interface on which the kernel will listen.")
151 help="Set the IP or interface on which the kernel will listen.")
152 def _ip_default(self):
152 def _ip_default(self):
153 if self.transport == 'ipc':
153 if self.transport == 'ipc':
154 if self.connection_file:
154 if self.connection_file:
155 return os.path.splitext(self.abs_connection_file)[0] + '-ipc'
155 return os.path.splitext(self.abs_connection_file)[0] + '-ipc'
156 else:
156 else:
157 return 'kernel-ipc'
157 return 'kernel-ipc'
158 else:
158 else:
159 return localhost()
159 return localhost()
160
160
161 hb_port = Integer(0, config=True, help="set the heartbeat port [default: random]")
161 hb_port = Integer(0, config=True, help="set the heartbeat port [default: random]")
162 shell_port = Integer(0, config=True, help="set the shell (ROUTER) port [default: random]")
162 shell_port = Integer(0, config=True, help="set the shell (ROUTER) port [default: random]")
163 iopub_port = Integer(0, config=True, help="set the iopub (PUB) port [default: random]")
163 iopub_port = Integer(0, config=True, help="set the iopub (PUB) port [default: random]")
164 stdin_port = Integer(0, config=True, help="set the stdin (ROUTER) port [default: random]")
164 stdin_port = Integer(0, config=True, help="set the stdin (ROUTER) port [default: random]")
165 control_port = Integer(0, config=True, help="set the control (ROUTER) port [default: random]")
165 control_port = Integer(0, config=True, help="set the control (ROUTER) port [default: random]")
166 connection_file = Unicode('', config=True,
166 connection_file = Unicode('', config=True,
167 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
167 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
168
168
169 This file will contain the IP, ports, and authentication key needed to connect
169 This file will contain the IP, ports, and authentication key needed to connect
170 clients to this kernel. By default, this file will be created in the security dir
170 clients to this kernel. By default, this file will be created in the security dir
171 of the current profile, but can be specified by absolute path.
171 of the current profile, but can be specified by absolute path.
172 """)
172 """)
173 @property
173 @property
174 def abs_connection_file(self):
174 def abs_connection_file(self):
175 if os.path.basename(self.connection_file) == self.connection_file:
175 if os.path.basename(self.connection_file) == self.connection_file:
176 return os.path.join(self.profile_dir.security_dir, self.connection_file)
176 return os.path.join(self.profile_dir.security_dir, self.connection_file)
177 else:
177 else:
178 return self.connection_file
178 return self.connection_file
179
179
180
180
181 # streams, etc.
181 # streams, etc.
182 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
182 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
183 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
183 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
184 outstream_class = DottedObjectName('IPython.kernel.zmq.iostream.OutStream',
184 outstream_class = DottedObjectName('IPython.kernel.zmq.iostream.OutStream',
185 config=True, help="The importstring for the OutStream factory")
185 config=True, help="The importstring for the OutStream factory")
186 displayhook_class = DottedObjectName('IPython.kernel.zmq.displayhook.ZMQDisplayHook',
186 displayhook_class = DottedObjectName('IPython.kernel.zmq.displayhook.ZMQDisplayHook',
187 config=True, help="The importstring for the DisplayHook factory")
187 config=True, help="The importstring for the DisplayHook factory")
188
188
189 # polling
189 # polling
190 parent_handle = Integer(0, config=True,
190 parent_handle = Integer(0, config=True,
191 help="""kill this process if its parent dies. On Windows, the argument
191 help="""kill this process if its parent dies. On Windows, the argument
192 specifies the HANDLE of the parent process, otherwise it is simply boolean.
192 specifies the HANDLE of the parent process, otherwise it is simply boolean.
193 """)
193 """)
194 interrupt = Integer(0, config=True,
194 interrupt = Integer(0, config=True,
195 help="""ONLY USED ON WINDOWS
195 help="""ONLY USED ON WINDOWS
196 Interrupt this process when the parent is signaled.
196 Interrupt this process when the parent is signaled.
197 """)
197 """)
198
198
199 def init_crash_handler(self):
199 def init_crash_handler(self):
200 # Install minimal exception handling
200 # Install minimal exception handling
201 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
201 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
202 ostream=sys.__stdout__)
202 ostream=sys.__stdout__)
203
203
204 def init_poller(self):
204 def init_poller(self):
205 if sys.platform == 'win32':
205 if sys.platform == 'win32':
206 if self.interrupt or self.parent_handle:
206 if self.interrupt or self.parent_handle:
207 self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
207 self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
208 elif self.parent_handle:
208 elif self.parent_handle:
209 self.poller = ParentPollerUnix()
209 self.poller = ParentPollerUnix()
210
210
211 def _bind_socket(self, s, port):
211 def _bind_socket(self, s, port):
212 iface = '%s://%s' % (self.transport, self.ip)
212 iface = '%s://%s' % (self.transport, self.ip)
213 if self.transport == 'tcp':
213 if self.transport == 'tcp':
214 if port <= 0:
214 if port <= 0:
215 port = s.bind_to_random_port(iface)
215 port = s.bind_to_random_port(iface)
216 else:
216 else:
217 s.bind("tcp://%s:%i" % (self.ip, port))
217 s.bind("tcp://%s:%i" % (self.ip, port))
218 elif self.transport == 'ipc':
218 elif self.transport == 'ipc':
219 if port <= 0:
219 if port <= 0:
220 port = 1
220 port = 1
221 path = "%s-%i" % (self.ip, port)
221 path = "%s-%i" % (self.ip, port)
222 while os.path.exists(path):
222 while os.path.exists(path):
223 port = port + 1
223 port = port + 1
224 path = "%s-%i" % (self.ip, port)
224 path = "%s-%i" % (self.ip, port)
225 else:
225 else:
226 path = "%s-%i" % (self.ip, port)
226 path = "%s-%i" % (self.ip, port)
227 s.bind("ipc://%s" % path)
227 s.bind("ipc://%s" % path)
228 return port
228 return port
229
229
230 def load_connection_file(self):
230 def load_connection_file(self):
231 """load ip/port/hmac config from JSON connection file"""
231 """load ip/port/hmac config from JSON connection file"""
232 try:
232 try:
233 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
233 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
234 except IOError:
234 except IOError:
235 self.log.debug("Connection file not found: %s", self.connection_file)
235 self.log.debug("Connection file not found: %s", self.connection_file)
236 # This means I own it, so I will clean it up:
236 # This means I own it, so I will clean it up:
237 atexit.register(self.cleanup_connection_file)
237 atexit.register(self.cleanup_connection_file)
238 return
238 return
239 self.log.debug(u"Loading connection file %s", fname)
239 self.log.debug(u"Loading connection file %s", fname)
240 with open(fname) as f:
240 with open(fname) as f:
241 s = f.read()
241 s = f.read()
242 cfg = json.loads(s)
242 cfg = json.loads(s)
243 self.transport = cfg.get('transport', self.transport)
243 self.transport = cfg.get('transport', self.transport)
244 if self.ip == self._ip_default() and 'ip' in cfg:
244 if self.ip == self._ip_default() and 'ip' in cfg:
245 # not overridden by config or cl_args
245 # not overridden by config or cl_args
246 self.ip = cfg['ip']
246 self.ip = cfg['ip']
247 for channel in ('hb', 'shell', 'iopub', 'stdin', 'control'):
247 for channel in ('hb', 'shell', 'iopub', 'stdin', 'control'):
248 name = channel + '_port'
248 name = channel + '_port'
249 if getattr(self, name) == 0 and name in cfg:
249 if getattr(self, name) == 0 and name in cfg:
250 # not overridden by config or cl_args
250 # not overridden by config or cl_args
251 setattr(self, name, cfg[name])
251 setattr(self, name, cfg[name])
252 if 'key' in cfg:
252 if 'key' in cfg:
253 self.config.Session.key = str_to_bytes(cfg['key'])
253 self.config.Session.key = str_to_bytes(cfg['key'])
254
254
255 def write_connection_file(self):
255 def write_connection_file(self):
256 """write connection info to JSON file"""
256 """write connection info to JSON file"""
257 cf = self.abs_connection_file
257 cf = self.abs_connection_file
258 self.log.debug("Writing connection file: %s", cf)
258 self.log.debug("Writing connection file: %s", cf)
259 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
259 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
260 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
260 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
261 iopub_port=self.iopub_port, control_port=self.control_port)
261 iopub_port=self.iopub_port, control_port=self.control_port)
262
262
263 def cleanup_connection_file(self):
263 def cleanup_connection_file(self):
264 cf = self.abs_connection_file
264 cf = self.abs_connection_file
265 self.log.debug("Cleaning up connection file: %s", cf)
265 self.log.debug("Cleaning up connection file: %s", cf)
266 try:
266 try:
267 os.remove(cf)
267 os.remove(cf)
268 except (IOError, OSError):
268 except (IOError, OSError):
269 pass
269 pass
270
270
271 self.cleanup_ipc_files()
271 self.cleanup_ipc_files()
272
272
273 def cleanup_ipc_files(self):
273 def cleanup_ipc_files(self):
274 """cleanup ipc files if we wrote them"""
274 """cleanup ipc files if we wrote them"""
275 if self.transport != 'ipc':
275 if self.transport != 'ipc':
276 return
276 return
277 for port in (self.shell_port, self.iopub_port, self.stdin_port, self.hb_port, self.control_port):
277 for port in (self.shell_port, self.iopub_port, self.stdin_port, self.hb_port, self.control_port):
278 ipcfile = "%s-%i" % (self.ip, port)
278 ipcfile = "%s-%i" % (self.ip, port)
279 try:
279 try:
280 os.remove(ipcfile)
280 os.remove(ipcfile)
281 except (IOError, OSError):
281 except (IOError, OSError):
282 pass
282 pass
283
283
284 def init_connection_file(self):
284 def init_connection_file(self):
285 if not self.connection_file:
285 if not self.connection_file:
286 self.connection_file = "kernel-%s.json"%os.getpid()
286 self.connection_file = "kernel-%s.json"%os.getpid()
287 try:
287 try:
288 self.load_connection_file()
288 self.load_connection_file()
289 except Exception:
289 except Exception:
290 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
290 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
291 self.exit(1)
291 self.exit(1)
292
292
293 def init_sockets(self):
293 def init_sockets(self):
294 # Create a context, a session, and the kernel sockets.
294 # Create a context, a session, and the kernel sockets.
295 self.log.info("Starting the kernel at pid: %i", os.getpid())
295 self.log.info("Starting the kernel at pid: %i", os.getpid())
296 context = zmq.Context.instance()
296 context = zmq.Context.instance()
297 # Uncomment this to try closing the context.
297 # Uncomment this to try closing the context.
298 # atexit.register(context.term)
298 # atexit.register(context.term)
299
299
300 self.shell_socket = context.socket(zmq.ROUTER)
300 self.shell_socket = context.socket(zmq.ROUTER)
301 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
301 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
302 self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port)
302 self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port)
303
303
304 self.iopub_socket = context.socket(zmq.PUB)
304 self.iopub_socket = context.socket(zmq.PUB)
305 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
305 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
306 self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port)
306 self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port)
307
307
308 self.stdin_socket = context.socket(zmq.ROUTER)
308 self.stdin_socket = context.socket(zmq.ROUTER)
309 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
309 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
310 self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port)
310 self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port)
311
311
312 self.control_socket = context.socket(zmq.ROUTER)
312 self.control_socket = context.socket(zmq.ROUTER)
313 self.control_port = self._bind_socket(self.control_socket, self.control_port)
313 self.control_port = self._bind_socket(self.control_socket, self.control_port)
314 self.log.debug("control ROUTER Channel on port: %i" % self.control_port)
314 self.log.debug("control ROUTER Channel on port: %i" % self.control_port)
315
315
316 def init_heartbeat(self):
316 def init_heartbeat(self):
317 """start the heart beating"""
317 """start the heart beating"""
318 # heartbeat doesn't share context, because it mustn't be blocked
318 # heartbeat doesn't share context, because it mustn't be blocked
319 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
319 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
320 hb_ctx = zmq.Context()
320 hb_ctx = zmq.Context()
321 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
321 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
322 self.hb_port = self.heartbeat.port
322 self.hb_port = self.heartbeat.port
323 self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port)
323 self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port)
324 self.heartbeat.start()
324 self.heartbeat.start()
325
325
326 def log_connection_info(self):
326 def log_connection_info(self):
327 """display connection info, and store ports"""
327 """display connection info, and store ports"""
328 basename = os.path.basename(self.connection_file)
328 basename = os.path.basename(self.connection_file)
329 if basename == self.connection_file or \
329 if basename == self.connection_file or \
330 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
330 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
331 # use shortname
331 # use shortname
332 tail = basename
332 tail = basename
333 if self.profile != 'default':
333 if self.profile != 'default':
334 tail += " --profile %s" % self.profile
334 tail += " --profile %s" % self.profile
335 else:
335 else:
336 tail = self.connection_file
336 tail = self.connection_file
337 lines = [
337 lines = [
338 "To connect another client to this kernel, use:",
338 "To connect another client to this kernel, use:",
339 " --existing %s" % tail,
339 " --existing %s" % tail,
340 ]
340 ]
341 # log connection info
341 # log connection info
342 # info-level, so often not shown.
342 # info-level, so often not shown.
343 # frontends should use the %connect_info magic
343 # frontends should use the %connect_info magic
344 # to see the connection info
344 # to see the connection info
345 for line in lines:
345 for line in lines:
346 self.log.info(line)
346 self.log.info(line)
347 # also raw print to the terminal if no parent_handle (`ipython kernel`)
347 # also raw print to the terminal if no parent_handle (`ipython kernel`)
348 if not self.parent_handle:
348 if not self.parent_handle:
349 io.rprint(_ctrl_c_message)
349 io.rprint(_ctrl_c_message)
350 for line in lines:
350 for line in lines:
351 io.rprint(line)
351 io.rprint(line)
352
352
353 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
353 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
354 stdin=self.stdin_port, hb=self.hb_port,
354 stdin=self.stdin_port, hb=self.hb_port,
355 control=self.control_port)
355 control=self.control_port)
356
356
357 def init_session(self):
357 def init_session(self):
358 """create our session object"""
358 """create our session object"""
359 default_secure(self.config)
359 default_secure(self.config)
360 self.session = Session(parent=self, username=u'kernel')
360 self.session = Session(parent=self, username=u'kernel')
361
361
362 def init_blackhole(self):
362 def init_blackhole(self):
363 """redirects stdout/stderr to devnull if necessary"""
363 """redirects stdout/stderr to devnull if necessary"""
364 if self.no_stdout or self.no_stderr:
364 if self.no_stdout or self.no_stderr:
365 blackhole = open(os.devnull, 'w')
365 blackhole = open(os.devnull, 'w')
366 if self.no_stdout:
366 if self.no_stdout:
367 sys.stdout = sys.__stdout__ = blackhole
367 sys.stdout = sys.__stdout__ = blackhole
368 if self.no_stderr:
368 if self.no_stderr:
369 sys.stderr = sys.__stderr__ = blackhole
369 sys.stderr = sys.__stderr__ = blackhole
370
370
371 def init_io(self):
371 def init_io(self):
372 """Redirect input streams and set a display hook."""
372 """Redirect input streams and set a display hook."""
373 if self.outstream_class:
373 if self.outstream_class:
374 outstream_factory = import_item(str(self.outstream_class))
374 outstream_factory = import_item(str(self.outstream_class))
375 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
375 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
376 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
376 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
377 if self.displayhook_class:
377 if self.displayhook_class:
378 displayhook_factory = import_item(str(self.displayhook_class))
378 displayhook_factory = import_item(str(self.displayhook_class))
379 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
379 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
380
380
381 def init_signal(self):
381 def init_signal(self):
382 signal.signal(signal.SIGINT, signal.SIG_IGN)
382 signal.signal(signal.SIGINT, signal.SIG_IGN)
383
383
384 def init_kernel(self):
384 def init_kernel(self):
385 """Create the Kernel object itself"""
385 """Create the Kernel object itself"""
386 shell_stream = ZMQStream(self.shell_socket)
386 shell_stream = ZMQStream(self.shell_socket)
387 control_stream = ZMQStream(self.control_socket)
387 control_stream = ZMQStream(self.control_socket)
388
388
389 kernel_factory = import_item(str(self.kernel_class))
389 kernel_factory = import_item(str(self.kernel_class))
390
390
391 kernel = kernel_factory(parent=self, session=self.session,
391 kernel = kernel_factory(parent=self, session=self.session,
392 shell_streams=[shell_stream, control_stream],
392 shell_streams=[shell_stream, control_stream],
393 iopub_socket=self.iopub_socket,
393 iopub_socket=self.iopub_socket,
394 stdin_socket=self.stdin_socket,
394 stdin_socket=self.stdin_socket,
395 log=self.log,
395 log=self.log,
396 profile_dir=self.profile_dir,
396 profile_dir=self.profile_dir,
397 user_ns=self.user_ns,
397 user_ns=self.user_ns,
398 )
398 )
399 kernel.record_ports(self.ports)
399 kernel.record_ports(self.ports)
400 self.kernel = kernel
400 self.kernel = kernel
401
401
402 def init_gui_pylab(self):
402 def init_gui_pylab(self):
403 """Enable GUI event loop integration, taking pylab into account."""
403 """Enable GUI event loop integration, taking pylab into account."""
404
404
405 # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
405 # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
406 # to ensure that any exception is printed straight to stderr.
406 # to ensure that any exception is printed straight to stderr.
407 # Normally _showtraceback associates the reply with an execution,
407 # Normally _showtraceback associates the reply with an execution,
408 # which means frontends will never draw it, as this exception
408 # which means frontends will never draw it, as this exception
409 # is not associated with any execute request.
409 # is not associated with any execute request.
410
410
411 shell = self.shell
411 shell = self.shell
412 _showtraceback = shell._showtraceback
412 _showtraceback = shell._showtraceback
413 try:
413 try:
414 # replace pyerr-sending traceback with stderr
414 # replace pyerr-sending traceback with stderr
415 def print_tb(etype, evalue, stb):
415 def print_tb(etype, evalue, stb):
416 print ("GUI event loop or pylab initialization failed",
416 print ("GUI event loop or pylab initialization failed",
417 file=io.stderr)
417 file=io.stderr)
418 print (shell.InteractiveTB.stb2text(stb), file=io.stderr)
418 print (shell.InteractiveTB.stb2text(stb), file=io.stderr)
419 shell._showtraceback = print_tb
419 shell._showtraceback = print_tb
420 InteractiveShellApp.init_gui_pylab(self)
420 InteractiveShellApp.init_gui_pylab(self)
421 finally:
421 finally:
422 shell._showtraceback = _showtraceback
422 shell._showtraceback = _showtraceback
423
423
424 def init_shell(self):
424 def init_shell(self):
425 self.shell = self.kernel.shell
425 self.shell = self.kernel.shell
426 self.shell.configurables.append(self)
426 self.shell.configurables.append(self)
427
427
428 @catch_config_error
428 @catch_config_error
429 def initialize(self, argv=None):
429 def initialize(self, argv=None):
430 super(IPKernelApp, self).initialize(argv)
430 super(IPKernelApp, self).initialize(argv)
431 self.init_blackhole()
431 self.init_blackhole()
432 self.init_connection_file()
432 self.init_connection_file()
433 self.init_session()
433 self.init_session()
434 self.init_poller()
434 self.init_poller()
435 self.init_sockets()
435 self.init_sockets()
436 self.init_heartbeat()
436 self.init_heartbeat()
437 # writing/displaying connection info must be *after* init_sockets/heartbeat
437 # writing/displaying connection info must be *after* init_sockets/heartbeat
438 self.log_connection_info()
438 self.log_connection_info()
439 self.write_connection_file()
439 self.write_connection_file()
440 self.init_io()
440 self.init_io()
441 self.init_signal()
441 self.init_signal()
442 self.init_kernel()
442 self.init_kernel()
443 # shell init steps
443 # shell init steps
444 self.init_path()
444 self.init_path()
445 self.init_shell()
445 self.init_shell()
446 self.init_gui_pylab()
446 self.init_gui_pylab()
447 self.init_extensions()
447 self.init_extensions()
448 self.init_code()
448 self.init_code()
449 # flush stdout/stderr, so that anything written to these streams during
449 # flush stdout/stderr, so that anything written to these streams during
450 # initialization do not get associated with the first execution request
450 # initialization do not get associated with the first execution request
451 sys.stdout.flush()
451 sys.stdout.flush()
452 sys.stderr.flush()
452 sys.stderr.flush()
453
453
454 def start(self):
454 def start(self):
455 if self.poller is not None:
455 if self.poller is not None:
456 self.poller.start()
456 self.poller.start()
457 self.kernel.start()
457 self.kernel.start()
458 try:
458 try:
459 ioloop.IOLoop.instance().start()
459 ioloop.IOLoop.instance().start()
460 except KeyboardInterrupt:
460 except KeyboardInterrupt:
461 pass
461 pass
462
462
463 launch_new_instance = IPKernelApp.launch_instance
463 launch_new_instance = IPKernelApp.launch_instance
464
464
465 def main():
465 def main():
466 """Run an IPKernel as an application"""
466 """Run an IPKernel as an application"""
467 app = IPKernelApp.instance()
467 app = IPKernelApp.instance()
468 app.initialize()
468 app.initialize()
469 app.start()
469 app.start()
470
470
471
471
472 if __name__ == '__main__':
472 if __name__ == '__main__':
473 main()
473 main()
@@ -1,623 +1,623 b''
1 """A ZMQ-based subclass of InteractiveShell.
1 """A ZMQ-based subclass of InteractiveShell.
2
2
3 This code is meant to ease the refactoring of the base InteractiveShell into
3 This code is meant to ease the refactoring of the base InteractiveShell into
4 something with a cleaner architecture for 2-process use, without actually
4 something with a cleaner architecture for 2-process use, without actually
5 breaking InteractiveShell itself. So we're doing something a bit ugly, where
5 breaking InteractiveShell itself. So we're doing something a bit ugly, where
6 we subclass and override what we want to fix. Once this is working well, we
6 we subclass and override what we want to fix. Once this is working well, we
7 can go back to the base class and refactor the code for a cleaner inheritance
7 can go back to the base class and refactor the code for a cleaner inheritance
8 implementation that doesn't rely on so much monkeypatching.
8 implementation that doesn't rely on so much monkeypatching.
9
9
10 But this lets us maintain a fully working IPython as we develop the new
10 But this lets us maintain a fully working IPython as we develop the new
11 machinery. This should thus be thought of as scaffolding.
11 machinery. This should thus be thought of as scaffolding.
12 """
12 """
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from __future__ import print_function
16 from __future__ import print_function
17
17
18 # Stdlib
18 # Stdlib
19 import os
19 import os
20 import sys
20 import sys
21 import time
21 import time
22
22
23 # System library imports
23 # System library imports
24 from zmq.eventloop import ioloop
24 from zmq.eventloop import ioloop
25
25
26 # Our own
26 # Our own
27 from IPython.core.interactiveshell import (
27 from IPython.core.interactiveshell import (
28 InteractiveShell, InteractiveShellABC
28 InteractiveShell, InteractiveShellABC
29 )
29 )
30 from IPython.core import page
30 from IPython.core import page
31 from IPython.core.autocall import ZMQExitAutocall
31 from IPython.core.autocall import ZMQExitAutocall
32 from IPython.core.displaypub import DisplayPublisher
32 from IPython.core.displaypub import DisplayPublisher
33 from IPython.core.error import UsageError
33 from IPython.core.error import UsageError
34 from IPython.core.magics import MacroToEdit, CodeMagics
34 from IPython.core.magics import MacroToEdit, CodeMagics
35 from IPython.core.magic import magics_class, line_magic, Magics
35 from IPython.core.magic import magics_class, line_magic, Magics
36 from IPython.core.payloadpage import install_payload_page
36 from IPython.core.payloadpage import install_payload_page
37 from IPython.display import display, Javascript
37 from IPython.display import display, Javascript
38 from IPython.kernel.inprocess.socket import SocketABC
38 from IPython.kernel.inprocess.socket import SocketABC
39 from IPython.kernel import (
39 from IPython.kernel import (
40 get_connection_file, get_connection_info, connect_qtconsole
40 get_connection_file, get_connection_info, connect_qtconsole
41 )
41 )
42 from IPython.testing.skipdoctest import skip_doctest
42 from IPython.testing.skipdoctest import skip_doctest
43 from IPython.utils import openpy
43 from IPython.utils import openpy
44 from IPython.utils.jsonutil import json_clean, encode_images
44 from IPython.utils.jsonutil import json_clean, encode_images
45 from IPython.utils.process import arg_split
45 from IPython.utils.process import arg_split
46 from IPython.utils import py3compat
46 from IPython.utils import py3compat
47 from IPython.utils.traitlets import Instance, Type, Dict, CBool, CBytes, Any
47 from IPython.utils.traitlets import Instance, Type, Dict, CBool, CBytes, Any
48 from IPython.utils.warn import error
48 from IPython.utils.warn import error
49 from IPython.kernel.zmq.displayhook import ZMQShellDisplayHook
49 from IPython.kernel.zmq.displayhook import ZMQShellDisplayHook
50 from IPython.kernel.zmq.datapub import ZMQDataPublisher
50 from IPython.kernel.zmq.datapub import ZMQDataPublisher
51 from IPython.kernel.zmq.session import extract_header
51 from IPython.kernel.zmq.session import extract_header
52 from IPython.kernel.comm import CommManager
52 from IPython.kernel.comm import CommManager
53 from session import Session
53 from .session import Session
54
54
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56 # Functions and classes
56 # Functions and classes
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58
58
59 class ZMQDisplayPublisher(DisplayPublisher):
59 class ZMQDisplayPublisher(DisplayPublisher):
60 """A display publisher that publishes data using a ZeroMQ PUB socket."""
60 """A display publisher that publishes data using a ZeroMQ PUB socket."""
61
61
62 session = Instance(Session)
62 session = Instance(Session)
63 pub_socket = Instance(SocketABC)
63 pub_socket = Instance(SocketABC)
64 parent_header = Dict({})
64 parent_header = Dict({})
65 topic = CBytes(b'display_data')
65 topic = CBytes(b'display_data')
66
66
67 def set_parent(self, parent):
67 def set_parent(self, parent):
68 """Set the parent for outbound messages."""
68 """Set the parent for outbound messages."""
69 self.parent_header = extract_header(parent)
69 self.parent_header = extract_header(parent)
70
70
71 def _flush_streams(self):
71 def _flush_streams(self):
72 """flush IO Streams prior to display"""
72 """flush IO Streams prior to display"""
73 sys.stdout.flush()
73 sys.stdout.flush()
74 sys.stderr.flush()
74 sys.stderr.flush()
75
75
76 def publish(self, source, data, metadata=None):
76 def publish(self, source, data, metadata=None):
77 self._flush_streams()
77 self._flush_streams()
78 if metadata is None:
78 if metadata is None:
79 metadata = {}
79 metadata = {}
80 self._validate_data(source, data, metadata)
80 self._validate_data(source, data, metadata)
81 content = {}
81 content = {}
82 content['source'] = source
82 content['source'] = source
83 content['data'] = encode_images(data)
83 content['data'] = encode_images(data)
84 content['metadata'] = metadata
84 content['metadata'] = metadata
85 self.session.send(
85 self.session.send(
86 self.pub_socket, u'display_data', json_clean(content),
86 self.pub_socket, u'display_data', json_clean(content),
87 parent=self.parent_header, ident=self.topic,
87 parent=self.parent_header, ident=self.topic,
88 )
88 )
89
89
90 def clear_output(self, wait=False):
90 def clear_output(self, wait=False):
91 content = dict(wait=wait)
91 content = dict(wait=wait)
92
92
93 print('\r', file=sys.stdout, end='')
93 print('\r', file=sys.stdout, end='')
94 print('\r', file=sys.stderr, end='')
94 print('\r', file=sys.stderr, end='')
95 self._flush_streams()
95 self._flush_streams()
96
96
97 self.session.send(
97 self.session.send(
98 self.pub_socket, u'clear_output', content,
98 self.pub_socket, u'clear_output', content,
99 parent=self.parent_header, ident=self.topic,
99 parent=self.parent_header, ident=self.topic,
100 )
100 )
101
101
102 @magics_class
102 @magics_class
103 class KernelMagics(Magics):
103 class KernelMagics(Magics):
104 #------------------------------------------------------------------------
104 #------------------------------------------------------------------------
105 # Magic overrides
105 # Magic overrides
106 #------------------------------------------------------------------------
106 #------------------------------------------------------------------------
107 # Once the base class stops inheriting from magic, this code needs to be
107 # Once the base class stops inheriting from magic, this code needs to be
108 # moved into a separate machinery as well. For now, at least isolate here
108 # moved into a separate machinery as well. For now, at least isolate here
109 # the magics which this class needs to implement differently from the base
109 # the magics which this class needs to implement differently from the base
110 # class, or that are unique to it.
110 # class, or that are unique to it.
111
111
112 @line_magic
112 @line_magic
113 def doctest_mode(self, parameter_s=''):
113 def doctest_mode(self, parameter_s=''):
114 """Toggle doctest mode on and off.
114 """Toggle doctest mode on and off.
115
115
116 This mode is intended to make IPython behave as much as possible like a
116 This mode is intended to make IPython behave as much as possible like a
117 plain Python shell, from the perspective of how its prompts, exceptions
117 plain Python shell, from the perspective of how its prompts, exceptions
118 and output look. This makes it easy to copy and paste parts of a
118 and output look. This makes it easy to copy and paste parts of a
119 session into doctests. It does so by:
119 session into doctests. It does so by:
120
120
121 - Changing the prompts to the classic ``>>>`` ones.
121 - Changing the prompts to the classic ``>>>`` ones.
122 - Changing the exception reporting mode to 'Plain'.
122 - Changing the exception reporting mode to 'Plain'.
123 - Disabling pretty-printing of output.
123 - Disabling pretty-printing of output.
124
124
125 Note that IPython also supports the pasting of code snippets that have
125 Note that IPython also supports the pasting of code snippets that have
126 leading '>>>' and '...' prompts in them. This means that you can paste
126 leading '>>>' and '...' prompts in them. This means that you can paste
127 doctests from files or docstrings (even if they have leading
127 doctests from files or docstrings (even if they have leading
128 whitespace), and the code will execute correctly. You can then use
128 whitespace), and the code will execute correctly. You can then use
129 '%history -t' to see the translated history; this will give you the
129 '%history -t' to see the translated history; this will give you the
130 input after removal of all the leading prompts and whitespace, which
130 input after removal of all the leading prompts and whitespace, which
131 can be pasted back into an editor.
131 can be pasted back into an editor.
132
132
133 With these features, you can switch into this mode easily whenever you
133 With these features, you can switch into this mode easily whenever you
134 need to do testing and changes to doctests, without having to leave
134 need to do testing and changes to doctests, without having to leave
135 your existing IPython session.
135 your existing IPython session.
136 """
136 """
137
137
138 from IPython.utils.ipstruct import Struct
138 from IPython.utils.ipstruct import Struct
139
139
140 # Shorthands
140 # Shorthands
141 shell = self.shell
141 shell = self.shell
142 disp_formatter = self.shell.display_formatter
142 disp_formatter = self.shell.display_formatter
143 ptformatter = disp_formatter.formatters['text/plain']
143 ptformatter = disp_formatter.formatters['text/plain']
144 # dstore is a data store kept in the instance metadata bag to track any
144 # dstore is a data store kept in the instance metadata bag to track any
145 # changes we make, so we can undo them later.
145 # changes we make, so we can undo them later.
146 dstore = shell.meta.setdefault('doctest_mode', Struct())
146 dstore = shell.meta.setdefault('doctest_mode', Struct())
147 save_dstore = dstore.setdefault
147 save_dstore = dstore.setdefault
148
148
149 # save a few values we'll need to recover later
149 # save a few values we'll need to recover later
150 mode = save_dstore('mode', False)
150 mode = save_dstore('mode', False)
151 save_dstore('rc_pprint', ptformatter.pprint)
151 save_dstore('rc_pprint', ptformatter.pprint)
152 save_dstore('rc_active_types',disp_formatter.active_types)
152 save_dstore('rc_active_types',disp_formatter.active_types)
153 save_dstore('xmode', shell.InteractiveTB.mode)
153 save_dstore('xmode', shell.InteractiveTB.mode)
154
154
155 if mode == False:
155 if mode == False:
156 # turn on
156 # turn on
157 ptformatter.pprint = False
157 ptformatter.pprint = False
158 disp_formatter.active_types = ['text/plain']
158 disp_formatter.active_types = ['text/plain']
159 shell.magic('xmode Plain')
159 shell.magic('xmode Plain')
160 else:
160 else:
161 # turn off
161 # turn off
162 ptformatter.pprint = dstore.rc_pprint
162 ptformatter.pprint = dstore.rc_pprint
163 disp_formatter.active_types = dstore.rc_active_types
163 disp_formatter.active_types = dstore.rc_active_types
164 shell.magic("xmode " + dstore.xmode)
164 shell.magic("xmode " + dstore.xmode)
165
165
166 # Store new mode and inform on console
166 # Store new mode and inform on console
167 dstore.mode = bool(1-int(mode))
167 dstore.mode = bool(1-int(mode))
168 mode_label = ['OFF','ON'][dstore.mode]
168 mode_label = ['OFF','ON'][dstore.mode]
169 print('Doctest mode is:', mode_label)
169 print('Doctest mode is:', mode_label)
170
170
171 # Send the payload back so that clients can modify their prompt display
171 # Send the payload back so that clients can modify their prompt display
172 payload = dict(
172 payload = dict(
173 source='doctest_mode',
173 source='doctest_mode',
174 mode=dstore.mode)
174 mode=dstore.mode)
175 shell.payload_manager.write_payload(payload)
175 shell.payload_manager.write_payload(payload)
176
176
177
177
178 _find_edit_target = CodeMagics._find_edit_target
178 _find_edit_target = CodeMagics._find_edit_target
179
179
180 @skip_doctest
180 @skip_doctest
181 @line_magic
181 @line_magic
182 def edit(self, parameter_s='', last_call=['','']):
182 def edit(self, parameter_s='', last_call=['','']):
183 """Bring up an editor and execute the resulting code.
183 """Bring up an editor and execute the resulting code.
184
184
185 Usage:
185 Usage:
186 %edit [options] [args]
186 %edit [options] [args]
187
187
188 %edit runs an external text editor. You will need to set the command for
188 %edit runs an external text editor. You will need to set the command for
189 this editor via the ``TerminalInteractiveShell.editor`` option in your
189 this editor via the ``TerminalInteractiveShell.editor`` option in your
190 configuration file before it will work.
190 configuration file before it will work.
191
191
192 This command allows you to conveniently edit multi-line code right in
192 This command allows you to conveniently edit multi-line code right in
193 your IPython session.
193 your IPython session.
194
194
195 If called without arguments, %edit opens up an empty editor with a
195 If called without arguments, %edit opens up an empty editor with a
196 temporary file and will execute the contents of this file when you
196 temporary file and will execute the contents of this file when you
197 close it (don't forget to save it!).
197 close it (don't forget to save it!).
198
198
199
199
200 Options:
200 Options:
201
201
202 -n <number>: open the editor at a specified line number. By default,
202 -n <number>: open the editor at a specified line number. By default,
203 the IPython editor hook uses the unix syntax 'editor +N filename', but
203 the IPython editor hook uses the unix syntax 'editor +N filename', but
204 you can configure this by providing your own modified hook if your
204 you can configure this by providing your own modified hook if your
205 favorite editor supports line-number specifications with a different
205 favorite editor supports line-number specifications with a different
206 syntax.
206 syntax.
207
207
208 -p: this will call the editor with the same data as the previous time
208 -p: this will call the editor with the same data as the previous time
209 it was used, regardless of how long ago (in your current session) it
209 it was used, regardless of how long ago (in your current session) it
210 was.
210 was.
211
211
212 -r: use 'raw' input. This option only applies to input taken from the
212 -r: use 'raw' input. This option only applies to input taken from the
213 user's history. By default, the 'processed' history is used, so that
213 user's history. By default, the 'processed' history is used, so that
214 magics are loaded in their transformed version to valid Python. If
214 magics are loaded in their transformed version to valid Python. If
215 this option is given, the raw input as typed as the command line is
215 this option is given, the raw input as typed as the command line is
216 used instead. When you exit the editor, it will be executed by
216 used instead. When you exit the editor, it will be executed by
217 IPython's own processor.
217 IPython's own processor.
218
218
219 -x: do not execute the edited code immediately upon exit. This is
219 -x: do not execute the edited code immediately upon exit. This is
220 mainly useful if you are editing programs which need to be called with
220 mainly useful if you are editing programs which need to be called with
221 command line arguments, which you can then do using %run.
221 command line arguments, which you can then do using %run.
222
222
223
223
224 Arguments:
224 Arguments:
225
225
226 If arguments are given, the following possibilites exist:
226 If arguments are given, the following possibilites exist:
227
227
228 - The arguments are numbers or pairs of colon-separated numbers (like
228 - The arguments are numbers or pairs of colon-separated numbers (like
229 1 4:8 9). These are interpreted as lines of previous input to be
229 1 4:8 9). These are interpreted as lines of previous input to be
230 loaded into the editor. The syntax is the same of the %macro command.
230 loaded into the editor. The syntax is the same of the %macro command.
231
231
232 - If the argument doesn't start with a number, it is evaluated as a
232 - If the argument doesn't start with a number, it is evaluated as a
233 variable and its contents loaded into the editor. You can thus edit
233 variable and its contents loaded into the editor. You can thus edit
234 any string which contains python code (including the result of
234 any string which contains python code (including the result of
235 previous edits).
235 previous edits).
236
236
237 - If the argument is the name of an object (other than a string),
237 - If the argument is the name of an object (other than a string),
238 IPython will try to locate the file where it was defined and open the
238 IPython will try to locate the file where it was defined and open the
239 editor at the point where it is defined. You can use `%edit function`
239 editor at the point where it is defined. You can use `%edit function`
240 to load an editor exactly at the point where 'function' is defined,
240 to load an editor exactly at the point where 'function' is defined,
241 edit it and have the file be executed automatically.
241 edit it and have the file be executed automatically.
242
242
243 If the object is a macro (see %macro for details), this opens up your
243 If the object is a macro (see %macro for details), this opens up your
244 specified editor with a temporary file containing the macro's data.
244 specified editor with a temporary file containing the macro's data.
245 Upon exit, the macro is reloaded with the contents of the file.
245 Upon exit, the macro is reloaded with the contents of the file.
246
246
247 Note: opening at an exact line is only supported under Unix, and some
247 Note: opening at an exact line is only supported under Unix, and some
248 editors (like kedit and gedit up to Gnome 2.8) do not understand the
248 editors (like kedit and gedit up to Gnome 2.8) do not understand the
249 '+NUMBER' parameter necessary for this feature. Good editors like
249 '+NUMBER' parameter necessary for this feature. Good editors like
250 (X)Emacs, vi, jed, pico and joe all do.
250 (X)Emacs, vi, jed, pico and joe all do.
251
251
252 - If the argument is not found as a variable, IPython will look for a
252 - If the argument is not found as a variable, IPython will look for a
253 file with that name (adding .py if necessary) and load it into the
253 file with that name (adding .py if necessary) and load it into the
254 editor. It will execute its contents with execfile() when you exit,
254 editor. It will execute its contents with execfile() when you exit,
255 loading any code in the file into your interactive namespace.
255 loading any code in the file into your interactive namespace.
256
256
257 After executing your code, %edit will return as output the code you
257 After executing your code, %edit will return as output the code you
258 typed in the editor (except when it was an existing file). This way
258 typed in the editor (except when it was an existing file). This way
259 you can reload the code in further invocations of %edit as a variable,
259 you can reload the code in further invocations of %edit as a variable,
260 via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of
260 via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of
261 the output.
261 the output.
262
262
263 Note that %edit is also available through the alias %ed.
263 Note that %edit is also available through the alias %ed.
264
264
265 This is an example of creating a simple function inside the editor and
265 This is an example of creating a simple function inside the editor and
266 then modifying it. First, start up the editor:
266 then modifying it. First, start up the editor:
267
267
268 In [1]: ed
268 In [1]: ed
269 Editing... done. Executing edited code...
269 Editing... done. Executing edited code...
270 Out[1]: 'def foo():n print "foo() was defined in an editing session"n'
270 Out[1]: 'def foo():n print "foo() was defined in an editing session"n'
271
271
272 We can then call the function foo():
272 We can then call the function foo():
273
273
274 In [2]: foo()
274 In [2]: foo()
275 foo() was defined in an editing session
275 foo() was defined in an editing session
276
276
277 Now we edit foo. IPython automatically loads the editor with the
277 Now we edit foo. IPython automatically loads the editor with the
278 (temporary) file where foo() was previously defined:
278 (temporary) file where foo() was previously defined:
279
279
280 In [3]: ed foo
280 In [3]: ed foo
281 Editing... done. Executing edited code...
281 Editing... done. Executing edited code...
282
282
283 And if we call foo() again we get the modified version:
283 And if we call foo() again we get the modified version:
284
284
285 In [4]: foo()
285 In [4]: foo()
286 foo() has now been changed!
286 foo() has now been changed!
287
287
288 Here is an example of how to edit a code snippet successive
288 Here is an example of how to edit a code snippet successive
289 times. First we call the editor:
289 times. First we call the editor:
290
290
291 In [5]: ed
291 In [5]: ed
292 Editing... done. Executing edited code...
292 Editing... done. Executing edited code...
293 hello
293 hello
294 Out[5]: "print 'hello'n"
294 Out[5]: "print 'hello'n"
295
295
296 Now we call it again with the previous output (stored in _):
296 Now we call it again with the previous output (stored in _):
297
297
298 In [6]: ed _
298 In [6]: ed _
299 Editing... done. Executing edited code...
299 Editing... done. Executing edited code...
300 hello world
300 hello world
301 Out[6]: "print 'hello world'n"
301 Out[6]: "print 'hello world'n"
302
302
303 Now we call it with the output #8 (stored in _8, also as Out[8]):
303 Now we call it with the output #8 (stored in _8, also as Out[8]):
304
304
305 In [7]: ed _8
305 In [7]: ed _8
306 Editing... done. Executing edited code...
306 Editing... done. Executing edited code...
307 hello again
307 hello again
308 Out[7]: "print 'hello again'n"
308 Out[7]: "print 'hello again'n"
309 """
309 """
310
310
311 opts,args = self.parse_options(parameter_s,'prn:')
311 opts,args = self.parse_options(parameter_s,'prn:')
312
312
313 try:
313 try:
314 filename, lineno, _ = CodeMagics._find_edit_target(self.shell, args, opts, last_call)
314 filename, lineno, _ = CodeMagics._find_edit_target(self.shell, args, opts, last_call)
315 except MacroToEdit as e:
315 except MacroToEdit as e:
316 # TODO: Implement macro editing over 2 processes.
316 # TODO: Implement macro editing over 2 processes.
317 print("Macro editing not yet implemented in 2-process model.")
317 print("Macro editing not yet implemented in 2-process model.")
318 return
318 return
319
319
320 # Make sure we send to the client an absolute path, in case the working
320 # Make sure we send to the client an absolute path, in case the working
321 # directory of client and kernel don't match
321 # directory of client and kernel don't match
322 filename = os.path.abspath(filename)
322 filename = os.path.abspath(filename)
323
323
324 payload = {
324 payload = {
325 'source' : 'edit_magic',
325 'source' : 'edit_magic',
326 'filename' : filename,
326 'filename' : filename,
327 'line_number' : lineno
327 'line_number' : lineno
328 }
328 }
329 self.shell.payload_manager.write_payload(payload)
329 self.shell.payload_manager.write_payload(payload)
330
330
331 # A few magics that are adapted to the specifics of using pexpect and a
331 # A few magics that are adapted to the specifics of using pexpect and a
332 # remote terminal
332 # remote terminal
333
333
334 @line_magic
334 @line_magic
335 def clear(self, arg_s):
335 def clear(self, arg_s):
336 """Clear the terminal."""
336 """Clear the terminal."""
337 if os.name == 'posix':
337 if os.name == 'posix':
338 self.shell.system("clear")
338 self.shell.system("clear")
339 else:
339 else:
340 self.shell.system("cls")
340 self.shell.system("cls")
341
341
342 if os.name == 'nt':
342 if os.name == 'nt':
343 # This is the usual name in windows
343 # This is the usual name in windows
344 cls = line_magic('cls')(clear)
344 cls = line_magic('cls')(clear)
345
345
346 # Terminal pagers won't work over pexpect, but we do have our own pager
346 # Terminal pagers won't work over pexpect, but we do have our own pager
347
347
348 @line_magic
348 @line_magic
349 def less(self, arg_s):
349 def less(self, arg_s):
350 """Show a file through the pager.
350 """Show a file through the pager.
351
351
352 Files ending in .py are syntax-highlighted."""
352 Files ending in .py are syntax-highlighted."""
353 if not arg_s:
353 if not arg_s:
354 raise UsageError('Missing filename.')
354 raise UsageError('Missing filename.')
355
355
356 cont = open(arg_s).read()
356 cont = open(arg_s).read()
357 if arg_s.endswith('.py'):
357 if arg_s.endswith('.py'):
358 cont = self.shell.pycolorize(openpy.read_py_file(arg_s, skip_encoding_cookie=False))
358 cont = self.shell.pycolorize(openpy.read_py_file(arg_s, skip_encoding_cookie=False))
359 else:
359 else:
360 cont = open(arg_s).read()
360 cont = open(arg_s).read()
361 page.page(cont)
361 page.page(cont)
362
362
363 more = line_magic('more')(less)
363 more = line_magic('more')(less)
364
364
365 # Man calls a pager, so we also need to redefine it
365 # Man calls a pager, so we also need to redefine it
366 if os.name == 'posix':
366 if os.name == 'posix':
367 @line_magic
367 @line_magic
368 def man(self, arg_s):
368 def man(self, arg_s):
369 """Find the man page for the given command and display in pager."""
369 """Find the man page for the given command and display in pager."""
370 page.page(self.shell.getoutput('man %s | col -b' % arg_s,
370 page.page(self.shell.getoutput('man %s | col -b' % arg_s,
371 split=False))
371 split=False))
372
372
373 @line_magic
373 @line_magic
374 def connect_info(self, arg_s):
374 def connect_info(self, arg_s):
375 """Print information for connecting other clients to this kernel
375 """Print information for connecting other clients to this kernel
376
376
377 It will print the contents of this session's connection file, as well as
377 It will print the contents of this session's connection file, as well as
378 shortcuts for local clients.
378 shortcuts for local clients.
379
379
380 In the simplest case, when called from the most recently launched kernel,
380 In the simplest case, when called from the most recently launched kernel,
381 secondary clients can be connected, simply with:
381 secondary clients can be connected, simply with:
382
382
383 $> ipython <app> --existing
383 $> ipython <app> --existing
384
384
385 """
385 """
386
386
387 from IPython.core.application import BaseIPythonApplication as BaseIPApp
387 from IPython.core.application import BaseIPythonApplication as BaseIPApp
388
388
389 if BaseIPApp.initialized():
389 if BaseIPApp.initialized():
390 app = BaseIPApp.instance()
390 app = BaseIPApp.instance()
391 security_dir = app.profile_dir.security_dir
391 security_dir = app.profile_dir.security_dir
392 profile = app.profile
392 profile = app.profile
393 else:
393 else:
394 profile = 'default'
394 profile = 'default'
395 security_dir = ''
395 security_dir = ''
396
396
397 try:
397 try:
398 connection_file = get_connection_file()
398 connection_file = get_connection_file()
399 info = get_connection_info(unpack=False)
399 info = get_connection_info(unpack=False)
400 except Exception as e:
400 except Exception as e:
401 error("Could not get connection info: %r" % e)
401 error("Could not get connection info: %r" % e)
402 return
402 return
403
403
404 # add profile flag for non-default profile
404 # add profile flag for non-default profile
405 profile_flag = "--profile %s" % profile if profile != 'default' else ""
405 profile_flag = "--profile %s" % profile if profile != 'default' else ""
406
406
407 # if it's in the security dir, truncate to basename
407 # if it's in the security dir, truncate to basename
408 if security_dir == os.path.dirname(connection_file):
408 if security_dir == os.path.dirname(connection_file):
409 connection_file = os.path.basename(connection_file)
409 connection_file = os.path.basename(connection_file)
410
410
411
411
412 print (info + '\n')
412 print (info + '\n')
413 print ("Paste the above JSON into a file, and connect with:\n"
413 print ("Paste the above JSON into a file, and connect with:\n"
414 " $> ipython <app> --existing <file>\n"
414 " $> ipython <app> --existing <file>\n"
415 "or, if you are local, you can connect with just:\n"
415 "or, if you are local, you can connect with just:\n"
416 " $> ipython <app> --existing {0} {1}\n"
416 " $> ipython <app> --existing {0} {1}\n"
417 "or even just:\n"
417 "or even just:\n"
418 " $> ipython <app> --existing {1}\n"
418 " $> ipython <app> --existing {1}\n"
419 "if this is the most recent IPython session you have started.".format(
419 "if this is the most recent IPython session you have started.".format(
420 connection_file, profile_flag
420 connection_file, profile_flag
421 )
421 )
422 )
422 )
423
423
424 @line_magic
424 @line_magic
425 def qtconsole(self, arg_s):
425 def qtconsole(self, arg_s):
426 """Open a qtconsole connected to this kernel.
426 """Open a qtconsole connected to this kernel.
427
427
428 Useful for connecting a qtconsole to running notebooks, for better
428 Useful for connecting a qtconsole to running notebooks, for better
429 debugging.
429 debugging.
430 """
430 """
431
431
432 # %qtconsole should imply bind_kernel for engines:
432 # %qtconsole should imply bind_kernel for engines:
433 try:
433 try:
434 from IPython.parallel import bind_kernel
434 from IPython.parallel import bind_kernel
435 except ImportError:
435 except ImportError:
436 # technically possible, because parallel has higher pyzmq min-version
436 # technically possible, because parallel has higher pyzmq min-version
437 pass
437 pass
438 else:
438 else:
439 bind_kernel()
439 bind_kernel()
440
440
441 try:
441 try:
442 p = connect_qtconsole(argv=arg_split(arg_s, os.name=='posix'))
442 p = connect_qtconsole(argv=arg_split(arg_s, os.name=='posix'))
443 except Exception as e:
443 except Exception as e:
444 error("Could not start qtconsole: %r" % e)
444 error("Could not start qtconsole: %r" % e)
445 return
445 return
446
446
447 @line_magic
447 @line_magic
448 def autosave(self, arg_s):
448 def autosave(self, arg_s):
449 """Set the autosave interval in the notebook (in seconds).
449 """Set the autosave interval in the notebook (in seconds).
450
450
451 The default value is 120, or two minutes.
451 The default value is 120, or two minutes.
452 ``%autosave 0`` will disable autosave.
452 ``%autosave 0`` will disable autosave.
453
453
454 This magic only has an effect when called from the notebook interface.
454 This magic only has an effect when called from the notebook interface.
455 It has no effect when called in a startup file.
455 It has no effect when called in a startup file.
456 """
456 """
457
457
458 try:
458 try:
459 interval = int(arg_s)
459 interval = int(arg_s)
460 except ValueError:
460 except ValueError:
461 raise UsageError("%%autosave requires an integer, got %r" % arg_s)
461 raise UsageError("%%autosave requires an integer, got %r" % arg_s)
462
462
463 # javascript wants milliseconds
463 # javascript wants milliseconds
464 milliseconds = 1000 * interval
464 milliseconds = 1000 * interval
465 display(Javascript("IPython.notebook.set_autosave_interval(%i)" % milliseconds),
465 display(Javascript("IPython.notebook.set_autosave_interval(%i)" % milliseconds),
466 include=['application/javascript']
466 include=['application/javascript']
467 )
467 )
468 if interval:
468 if interval:
469 print("Autosaving every %i seconds" % interval)
469 print("Autosaving every %i seconds" % interval)
470 else:
470 else:
471 print("Autosave disabled")
471 print("Autosave disabled")
472
472
473
473
474 class ZMQInteractiveShell(InteractiveShell):
474 class ZMQInteractiveShell(InteractiveShell):
475 """A subclass of InteractiveShell for ZMQ."""
475 """A subclass of InteractiveShell for ZMQ."""
476
476
477 displayhook_class = Type(ZMQShellDisplayHook)
477 displayhook_class = Type(ZMQShellDisplayHook)
478 display_pub_class = Type(ZMQDisplayPublisher)
478 display_pub_class = Type(ZMQDisplayPublisher)
479 data_pub_class = Type(ZMQDataPublisher)
479 data_pub_class = Type(ZMQDataPublisher)
480 kernel = Any()
480 kernel = Any()
481 parent_header = Any()
481 parent_header = Any()
482
482
483 # Override the traitlet in the parent class, because there's no point using
483 # Override the traitlet in the parent class, because there's no point using
484 # readline for the kernel. Can be removed when the readline code is moved
484 # readline for the kernel. Can be removed when the readline code is moved
485 # to the terminal frontend.
485 # to the terminal frontend.
486 colors_force = CBool(True)
486 colors_force = CBool(True)
487 readline_use = CBool(False)
487 readline_use = CBool(False)
488 # autoindent has no meaning in a zmqshell, and attempting to enable it
488 # autoindent has no meaning in a zmqshell, and attempting to enable it
489 # will print a warning in the absence of readline.
489 # will print a warning in the absence of readline.
490 autoindent = CBool(False)
490 autoindent = CBool(False)
491
491
492 exiter = Instance(ZMQExitAutocall)
492 exiter = Instance(ZMQExitAutocall)
493 def _exiter_default(self):
493 def _exiter_default(self):
494 return ZMQExitAutocall(self)
494 return ZMQExitAutocall(self)
495
495
496 def _exit_now_changed(self, name, old, new):
496 def _exit_now_changed(self, name, old, new):
497 """stop eventloop when exit_now fires"""
497 """stop eventloop when exit_now fires"""
498 if new:
498 if new:
499 loop = ioloop.IOLoop.instance()
499 loop = ioloop.IOLoop.instance()
500 loop.add_timeout(time.time()+0.1, loop.stop)
500 loop.add_timeout(time.time()+0.1, loop.stop)
501
501
502 keepkernel_on_exit = None
502 keepkernel_on_exit = None
503
503
504 # Over ZeroMQ, GUI control isn't done with PyOS_InputHook as there is no
504 # Over ZeroMQ, GUI control isn't done with PyOS_InputHook as there is no
505 # interactive input being read; we provide event loop support in ipkernel
505 # interactive input being read; we provide event loop support in ipkernel
506 @staticmethod
506 @staticmethod
507 def enable_gui(gui):
507 def enable_gui(gui):
508 from .eventloops import enable_gui as real_enable_gui
508 from .eventloops import enable_gui as real_enable_gui
509 try:
509 try:
510 real_enable_gui(gui)
510 real_enable_gui(gui)
511 except ValueError as e:
511 except ValueError as e:
512 raise UsageError("%s" % e)
512 raise UsageError("%s" % e)
513
513
514 def init_environment(self):
514 def init_environment(self):
515 """Configure the user's environment.
515 """Configure the user's environment.
516
516
517 """
517 """
518 env = os.environ
518 env = os.environ
519 # These two ensure 'ls' produces nice coloring on BSD-derived systems
519 # These two ensure 'ls' produces nice coloring on BSD-derived systems
520 env['TERM'] = 'xterm-color'
520 env['TERM'] = 'xterm-color'
521 env['CLICOLOR'] = '1'
521 env['CLICOLOR'] = '1'
522 # Since normal pagers don't work at all (over pexpect we don't have
522 # Since normal pagers don't work at all (over pexpect we don't have
523 # single-key control of the subprocess), try to disable paging in
523 # single-key control of the subprocess), try to disable paging in
524 # subprocesses as much as possible.
524 # subprocesses as much as possible.
525 env['PAGER'] = 'cat'
525 env['PAGER'] = 'cat'
526 env['GIT_PAGER'] = 'cat'
526 env['GIT_PAGER'] = 'cat'
527
527
528 # And install the payload version of page.
528 # And install the payload version of page.
529 install_payload_page()
529 install_payload_page()
530
530
531 def auto_rewrite_input(self, cmd):
531 def auto_rewrite_input(self, cmd):
532 """Called to show the auto-rewritten input for autocall and friends.
532 """Called to show the auto-rewritten input for autocall and friends.
533
533
534 FIXME: this payload is currently not correctly processed by the
534 FIXME: this payload is currently not correctly processed by the
535 frontend.
535 frontend.
536 """
536 """
537 new = self.prompt_manager.render('rewrite') + cmd
537 new = self.prompt_manager.render('rewrite') + cmd
538 payload = dict(
538 payload = dict(
539 source='auto_rewrite_input',
539 source='auto_rewrite_input',
540 transformed_input=new,
540 transformed_input=new,
541 )
541 )
542 self.payload_manager.write_payload(payload)
542 self.payload_manager.write_payload(payload)
543
543
544 def ask_exit(self):
544 def ask_exit(self):
545 """Engage the exit actions."""
545 """Engage the exit actions."""
546 self.exit_now = True
546 self.exit_now = True
547 payload = dict(
547 payload = dict(
548 source='ask_exit',
548 source='ask_exit',
549 exit=True,
549 exit=True,
550 keepkernel=self.keepkernel_on_exit,
550 keepkernel=self.keepkernel_on_exit,
551 )
551 )
552 self.payload_manager.write_payload(payload)
552 self.payload_manager.write_payload(payload)
553
553
554 def _showtraceback(self, etype, evalue, stb):
554 def _showtraceback(self, etype, evalue, stb):
555
555
556 exc_content = {
556 exc_content = {
557 u'traceback' : stb,
557 u'traceback' : stb,
558 u'ename' : unicode(etype.__name__),
558 u'ename' : unicode(etype.__name__),
559 u'evalue' : py3compat.safe_unicode(evalue),
559 u'evalue' : py3compat.safe_unicode(evalue),
560 }
560 }
561
561
562 dh = self.displayhook
562 dh = self.displayhook
563 # Send exception info over pub socket for other clients than the caller
563 # Send exception info over pub socket for other clients than the caller
564 # to pick up
564 # to pick up
565 topic = None
565 topic = None
566 if dh.topic:
566 if dh.topic:
567 topic = dh.topic.replace(b'pyout', b'pyerr')
567 topic = dh.topic.replace(b'pyout', b'pyerr')
568
568
569 exc_msg = dh.session.send(dh.pub_socket, u'pyerr', json_clean(exc_content), dh.parent_header, ident=topic)
569 exc_msg = dh.session.send(dh.pub_socket, u'pyerr', json_clean(exc_content), dh.parent_header, ident=topic)
570
570
571 # FIXME - Hack: store exception info in shell object. Right now, the
571 # FIXME - Hack: store exception info in shell object. Right now, the
572 # caller is reading this info after the fact, we need to fix this logic
572 # caller is reading this info after the fact, we need to fix this logic
573 # to remove this hack. Even uglier, we need to store the error status
573 # to remove this hack. Even uglier, we need to store the error status
574 # here, because in the main loop, the logic that sets it is being
574 # here, because in the main loop, the logic that sets it is being
575 # skipped because runlines swallows the exceptions.
575 # skipped because runlines swallows the exceptions.
576 exc_content[u'status'] = u'error'
576 exc_content[u'status'] = u'error'
577 self._reply_content = exc_content
577 self._reply_content = exc_content
578 # /FIXME
578 # /FIXME
579
579
580 return exc_content
580 return exc_content
581
581
582 def set_next_input(self, text):
582 def set_next_input(self, text):
583 """Send the specified text to the frontend to be presented at the next
583 """Send the specified text to the frontend to be presented at the next
584 input cell."""
584 input cell."""
585 payload = dict(
585 payload = dict(
586 source='set_next_input',
586 source='set_next_input',
587 text=text
587 text=text
588 )
588 )
589 self.payload_manager.write_payload(payload)
589 self.payload_manager.write_payload(payload)
590
590
591 def set_parent(self, parent):
591 def set_parent(self, parent):
592 """Set the parent header for associating output with its triggering input"""
592 """Set the parent header for associating output with its triggering input"""
593 self.parent_header = parent
593 self.parent_header = parent
594 self.displayhook.set_parent(parent)
594 self.displayhook.set_parent(parent)
595 self.display_pub.set_parent(parent)
595 self.display_pub.set_parent(parent)
596 self.data_pub.set_parent(parent)
596 self.data_pub.set_parent(parent)
597 try:
597 try:
598 sys.stdout.set_parent(parent)
598 sys.stdout.set_parent(parent)
599 except AttributeError:
599 except AttributeError:
600 pass
600 pass
601 try:
601 try:
602 sys.stderr.set_parent(parent)
602 sys.stderr.set_parent(parent)
603 except AttributeError:
603 except AttributeError:
604 pass
604 pass
605
605
606 def get_parent(self):
606 def get_parent(self):
607 return self.parent_header
607 return self.parent_header
608
608
609 #-------------------------------------------------------------------------
609 #-------------------------------------------------------------------------
610 # Things related to magics
610 # Things related to magics
611 #-------------------------------------------------------------------------
611 #-------------------------------------------------------------------------
612
612
613 def init_magics(self):
613 def init_magics(self):
614 super(ZMQInteractiveShell, self).init_magics()
614 super(ZMQInteractiveShell, self).init_magics()
615 self.register_magics(KernelMagics)
615 self.register_magics(KernelMagics)
616 self.magics_manager.register_alias('ed', 'edit')
616 self.magics_manager.register_alias('ed', 'edit')
617
617
618 def init_comms(self):
618 def init_comms(self):
619 self.comm_manager = CommManager(shell=self, parent=self)
619 self.comm_manager = CommManager(shell=self, parent=self)
620 self.configurables.append(self.comm_manager)
620 self.configurables.append(self.comm_manager)
621
621
622
622
623 InteractiveShellABC.register(ZMQInteractiveShell)
623 InteractiveShellABC.register(ZMQInteractiveShell)
@@ -1,7 +1,7 b''
1 """Utilities for converting notebooks to and from different formats."""
1 """Utilities for converting notebooks to and from different formats."""
2
2
3 from .exporters import *
3 from .exporters import *
4 import filters
4 from . import filters
5 import preprocessors
5 from . import preprocessors
6 import postprocessors
6 from . import postprocessors
7 import writers
7 from . import writers
@@ -1,517 +1,517 b''
1 """Tests for parallel client.py
1 """Tests for parallel client.py
2
2
3 Authors:
3 Authors:
4
4
5 * Min RK
5 * Min RK
6 """
6 """
7
7
8 #-------------------------------------------------------------------------------
8 #-------------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-------------------------------------------------------------------------------
13 #-------------------------------------------------------------------------------
14
14
15 #-------------------------------------------------------------------------------
15 #-------------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-------------------------------------------------------------------------------
17 #-------------------------------------------------------------------------------
18
18
19 from __future__ import division
19 from __future__ import division
20
20
21 import time
21 import time
22 from datetime import datetime
22 from datetime import datetime
23 from tempfile import mktemp
23 from tempfile import mktemp
24
24
25 import zmq
25 import zmq
26
26
27 from IPython import parallel
27 from IPython import parallel
28 from IPython.parallel.client import client as clientmod
28 from IPython.parallel.client import client as clientmod
29 from IPython.parallel import error
29 from IPython.parallel import error
30 from IPython.parallel import AsyncResult, AsyncHubResult
30 from IPython.parallel import AsyncResult, AsyncHubResult
31 from IPython.parallel import LoadBalancedView, DirectView
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 def setup():
35 def setup():
36 add_engines(4, total=True)
36 add_engines(4, total=True)
37
37
38 class TestClient(ClusterTestCase):
38 class TestClient(ClusterTestCase):
39
39
40 def test_ids(self):
40 def test_ids(self):
41 n = len(self.client.ids)
41 n = len(self.client.ids)
42 self.add_engines(2)
42 self.add_engines(2)
43 self.assertEqual(len(self.client.ids), n+2)
43 self.assertEqual(len(self.client.ids), n+2)
44
44
45 def test_view_indexing(self):
45 def test_view_indexing(self):
46 """test index access for views"""
46 """test index access for views"""
47 self.minimum_engines(4)
47 self.minimum_engines(4)
48 targets = self.client._build_targets('all')[-1]
48 targets = self.client._build_targets('all')[-1]
49 v = self.client[:]
49 v = self.client[:]
50 self.assertEqual(v.targets, targets)
50 self.assertEqual(v.targets, targets)
51 t = self.client.ids[2]
51 t = self.client.ids[2]
52 v = self.client[t]
52 v = self.client[t]
53 self.assertTrue(isinstance(v, DirectView))
53 self.assertTrue(isinstance(v, DirectView))
54 self.assertEqual(v.targets, t)
54 self.assertEqual(v.targets, t)
55 t = self.client.ids[2:4]
55 t = self.client.ids[2:4]
56 v = self.client[t]
56 v = self.client[t]
57 self.assertTrue(isinstance(v, DirectView))
57 self.assertTrue(isinstance(v, DirectView))
58 self.assertEqual(v.targets, t)
58 self.assertEqual(v.targets, t)
59 v = self.client[::2]
59 v = self.client[::2]
60 self.assertTrue(isinstance(v, DirectView))
60 self.assertTrue(isinstance(v, DirectView))
61 self.assertEqual(v.targets, targets[::2])
61 self.assertEqual(v.targets, targets[::2])
62 v = self.client[1::3]
62 v = self.client[1::3]
63 self.assertTrue(isinstance(v, DirectView))
63 self.assertTrue(isinstance(v, DirectView))
64 self.assertEqual(v.targets, targets[1::3])
64 self.assertEqual(v.targets, targets[1::3])
65 v = self.client[:-3]
65 v = self.client[:-3]
66 self.assertTrue(isinstance(v, DirectView))
66 self.assertTrue(isinstance(v, DirectView))
67 self.assertEqual(v.targets, targets[:-3])
67 self.assertEqual(v.targets, targets[:-3])
68 v = self.client[-1]
68 v = self.client[-1]
69 self.assertTrue(isinstance(v, DirectView))
69 self.assertTrue(isinstance(v, DirectView))
70 self.assertEqual(v.targets, targets[-1])
70 self.assertEqual(v.targets, targets[-1])
71 self.assertRaises(TypeError, lambda : self.client[None])
71 self.assertRaises(TypeError, lambda : self.client[None])
72
72
73 def test_lbview_targets(self):
73 def test_lbview_targets(self):
74 """test load_balanced_view targets"""
74 """test load_balanced_view targets"""
75 v = self.client.load_balanced_view()
75 v = self.client.load_balanced_view()
76 self.assertEqual(v.targets, None)
76 self.assertEqual(v.targets, None)
77 v = self.client.load_balanced_view(-1)
77 v = self.client.load_balanced_view(-1)
78 self.assertEqual(v.targets, [self.client.ids[-1]])
78 self.assertEqual(v.targets, [self.client.ids[-1]])
79 v = self.client.load_balanced_view('all')
79 v = self.client.load_balanced_view('all')
80 self.assertEqual(v.targets, None)
80 self.assertEqual(v.targets, None)
81
81
82 def test_dview_targets(self):
82 def test_dview_targets(self):
83 """test direct_view targets"""
83 """test direct_view targets"""
84 v = self.client.direct_view()
84 v = self.client.direct_view()
85 self.assertEqual(v.targets, 'all')
85 self.assertEqual(v.targets, 'all')
86 v = self.client.direct_view('all')
86 v = self.client.direct_view('all')
87 self.assertEqual(v.targets, 'all')
87 self.assertEqual(v.targets, 'all')
88 v = self.client.direct_view(-1)
88 v = self.client.direct_view(-1)
89 self.assertEqual(v.targets, self.client.ids[-1])
89 self.assertEqual(v.targets, self.client.ids[-1])
90
90
91 def test_lazy_all_targets(self):
91 def test_lazy_all_targets(self):
92 """test lazy evaluation of rc.direct_view('all')"""
92 """test lazy evaluation of rc.direct_view('all')"""
93 v = self.client.direct_view()
93 v = self.client.direct_view()
94 self.assertEqual(v.targets, 'all')
94 self.assertEqual(v.targets, 'all')
95
95
96 def double(x):
96 def double(x):
97 return x*2
97 return x*2
98 seq = range(100)
98 seq = range(100)
99 ref = [ double(x) for x in seq ]
99 ref = [ double(x) for x in seq ]
100
100
101 # add some engines, which should be used
101 # add some engines, which should be used
102 self.add_engines(1)
102 self.add_engines(1)
103 n1 = len(self.client.ids)
103 n1 = len(self.client.ids)
104
104
105 # simple apply
105 # simple apply
106 r = v.apply_sync(lambda : 1)
106 r = v.apply_sync(lambda : 1)
107 self.assertEqual(r, [1] * n1)
107 self.assertEqual(r, [1] * n1)
108
108
109 # map goes through remotefunction
109 # map goes through remotefunction
110 r = v.map_sync(double, seq)
110 r = v.map_sync(double, seq)
111 self.assertEqual(r, ref)
111 self.assertEqual(r, ref)
112
112
113 # add a couple more engines, and try again
113 # add a couple more engines, and try again
114 self.add_engines(2)
114 self.add_engines(2)
115 n2 = len(self.client.ids)
115 n2 = len(self.client.ids)
116 self.assertNotEqual(n2, n1)
116 self.assertNotEqual(n2, n1)
117
117
118 # apply
118 # apply
119 r = v.apply_sync(lambda : 1)
119 r = v.apply_sync(lambda : 1)
120 self.assertEqual(r, [1] * n2)
120 self.assertEqual(r, [1] * n2)
121
121
122 # map
122 # map
123 r = v.map_sync(double, seq)
123 r = v.map_sync(double, seq)
124 self.assertEqual(r, ref)
124 self.assertEqual(r, ref)
125
125
126 def test_targets(self):
126 def test_targets(self):
127 """test various valid targets arguments"""
127 """test various valid targets arguments"""
128 build = self.client._build_targets
128 build = self.client._build_targets
129 ids = self.client.ids
129 ids = self.client.ids
130 idents,targets = build(None)
130 idents,targets = build(None)
131 self.assertEqual(ids, targets)
131 self.assertEqual(ids, targets)
132
132
133 def test_clear(self):
133 def test_clear(self):
134 """test clear behavior"""
134 """test clear behavior"""
135 self.minimum_engines(2)
135 self.minimum_engines(2)
136 v = self.client[:]
136 v = self.client[:]
137 v.block=True
137 v.block=True
138 v.push(dict(a=5))
138 v.push(dict(a=5))
139 v.pull('a')
139 v.pull('a')
140 id0 = self.client.ids[-1]
140 id0 = self.client.ids[-1]
141 self.client.clear(targets=id0, block=True)
141 self.client.clear(targets=id0, block=True)
142 a = self.client[:-1].get('a')
142 a = self.client[:-1].get('a')
143 self.assertRaisesRemote(NameError, self.client[id0].get, 'a')
143 self.assertRaisesRemote(NameError, self.client[id0].get, 'a')
144 self.client.clear(block=True)
144 self.client.clear(block=True)
145 for i in self.client.ids:
145 for i in self.client.ids:
146 self.assertRaisesRemote(NameError, self.client[i].get, 'a')
146 self.assertRaisesRemote(NameError, self.client[i].get, 'a')
147
147
148 def test_get_result(self):
148 def test_get_result(self):
149 """test getting results from the Hub."""
149 """test getting results from the Hub."""
150 c = clientmod.Client(profile='iptest')
150 c = clientmod.Client(profile='iptest')
151 t = c.ids[-1]
151 t = c.ids[-1]
152 ar = c[t].apply_async(wait, 1)
152 ar = c[t].apply_async(wait, 1)
153 # give the monitor time to notice the message
153 # give the monitor time to notice the message
154 time.sleep(.25)
154 time.sleep(.25)
155 ahr = self.client.get_result(ar.msg_ids[0])
155 ahr = self.client.get_result(ar.msg_ids[0])
156 self.assertTrue(isinstance(ahr, AsyncHubResult))
156 self.assertTrue(isinstance(ahr, AsyncHubResult))
157 self.assertEqual(ahr.get(), ar.get())
157 self.assertEqual(ahr.get(), ar.get())
158 ar2 = self.client.get_result(ar.msg_ids[0])
158 ar2 = self.client.get_result(ar.msg_ids[0])
159 self.assertFalse(isinstance(ar2, AsyncHubResult))
159 self.assertFalse(isinstance(ar2, AsyncHubResult))
160 c.close()
160 c.close()
161
161
162 def test_get_execute_result(self):
162 def test_get_execute_result(self):
163 """test getting execute results from the Hub."""
163 """test getting execute results from the Hub."""
164 c = clientmod.Client(profile='iptest')
164 c = clientmod.Client(profile='iptest')
165 t = c.ids[-1]
165 t = c.ids[-1]
166 cell = '\n'.join([
166 cell = '\n'.join([
167 'import time',
167 'import time',
168 'time.sleep(0.25)',
168 'time.sleep(0.25)',
169 '5'
169 '5'
170 ])
170 ])
171 ar = c[t].execute("import time; time.sleep(1)", silent=False)
171 ar = c[t].execute("import time; time.sleep(1)", silent=False)
172 # give the monitor time to notice the message
172 # give the monitor time to notice the message
173 time.sleep(.25)
173 time.sleep(.25)
174 ahr = self.client.get_result(ar.msg_ids[0])
174 ahr = self.client.get_result(ar.msg_ids[0])
175 self.assertTrue(isinstance(ahr, AsyncHubResult))
175 self.assertTrue(isinstance(ahr, AsyncHubResult))
176 self.assertEqual(ahr.get().pyout, ar.get().pyout)
176 self.assertEqual(ahr.get().pyout, ar.get().pyout)
177 ar2 = self.client.get_result(ar.msg_ids[0])
177 ar2 = self.client.get_result(ar.msg_ids[0])
178 self.assertFalse(isinstance(ar2, AsyncHubResult))
178 self.assertFalse(isinstance(ar2, AsyncHubResult))
179 c.close()
179 c.close()
180
180
181 def test_ids_list(self):
181 def test_ids_list(self):
182 """test client.ids"""
182 """test client.ids"""
183 ids = self.client.ids
183 ids = self.client.ids
184 self.assertEqual(ids, self.client._ids)
184 self.assertEqual(ids, self.client._ids)
185 self.assertFalse(ids is self.client._ids)
185 self.assertFalse(ids is self.client._ids)
186 ids.remove(ids[-1])
186 ids.remove(ids[-1])
187 self.assertNotEqual(ids, self.client._ids)
187 self.assertNotEqual(ids, self.client._ids)
188
188
189 def test_queue_status(self):
189 def test_queue_status(self):
190 ids = self.client.ids
190 ids = self.client.ids
191 id0 = ids[0]
191 id0 = ids[0]
192 qs = self.client.queue_status(targets=id0)
192 qs = self.client.queue_status(targets=id0)
193 self.assertTrue(isinstance(qs, dict))
193 self.assertTrue(isinstance(qs, dict))
194 self.assertEqual(sorted(qs.keys()), ['completed', 'queue', 'tasks'])
194 self.assertEqual(sorted(qs.keys()), ['completed', 'queue', 'tasks'])
195 allqs = self.client.queue_status()
195 allqs = self.client.queue_status()
196 self.assertTrue(isinstance(allqs, dict))
196 self.assertTrue(isinstance(allqs, dict))
197 intkeys = list(allqs.keys())
197 intkeys = list(allqs.keys())
198 intkeys.remove('unassigned')
198 intkeys.remove('unassigned')
199 self.assertEqual(sorted(intkeys), sorted(self.client.ids))
199 self.assertEqual(sorted(intkeys), sorted(self.client.ids))
200 unassigned = allqs.pop('unassigned')
200 unassigned = allqs.pop('unassigned')
201 for eid,qs in allqs.items():
201 for eid,qs in allqs.items():
202 self.assertTrue(isinstance(qs, dict))
202 self.assertTrue(isinstance(qs, dict))
203 self.assertEqual(sorted(qs.keys()), ['completed', 'queue', 'tasks'])
203 self.assertEqual(sorted(qs.keys()), ['completed', 'queue', 'tasks'])
204
204
205 def test_shutdown(self):
205 def test_shutdown(self):
206 ids = self.client.ids
206 ids = self.client.ids
207 id0 = ids[0]
207 id0 = ids[0]
208 self.client.shutdown(id0, block=True)
208 self.client.shutdown(id0, block=True)
209 while id0 in self.client.ids:
209 while id0 in self.client.ids:
210 time.sleep(0.1)
210 time.sleep(0.1)
211 self.client.spin()
211 self.client.spin()
212
212
213 self.assertRaises(IndexError, lambda : self.client[id0])
213 self.assertRaises(IndexError, lambda : self.client[id0])
214
214
215 def test_result_status(self):
215 def test_result_status(self):
216 pass
216 pass
217 # to be written
217 # to be written
218
218
219 def test_db_query_dt(self):
219 def test_db_query_dt(self):
220 """test db query by date"""
220 """test db query by date"""
221 hist = self.client.hub_history()
221 hist = self.client.hub_history()
222 middle = self.client.db_query({'msg_id' : hist[len(hist)//2]})[0]
222 middle = self.client.db_query({'msg_id' : hist[len(hist)//2]})[0]
223 tic = middle['submitted']
223 tic = middle['submitted']
224 before = self.client.db_query({'submitted' : {'$lt' : tic}})
224 before = self.client.db_query({'submitted' : {'$lt' : tic}})
225 after = self.client.db_query({'submitted' : {'$gte' : tic}})
225 after = self.client.db_query({'submitted' : {'$gte' : tic}})
226 self.assertEqual(len(before)+len(after),len(hist))
226 self.assertEqual(len(before)+len(after),len(hist))
227 for b in before:
227 for b in before:
228 self.assertTrue(b['submitted'] < tic)
228 self.assertTrue(b['submitted'] < tic)
229 for a in after:
229 for a in after:
230 self.assertTrue(a['submitted'] >= tic)
230 self.assertTrue(a['submitted'] >= tic)
231 same = self.client.db_query({'submitted' : tic})
231 same = self.client.db_query({'submitted' : tic})
232 for s in same:
232 for s in same:
233 self.assertTrue(s['submitted'] == tic)
233 self.assertTrue(s['submitted'] == tic)
234
234
235 def test_db_query_keys(self):
235 def test_db_query_keys(self):
236 """test extracting subset of record keys"""
236 """test extracting subset of record keys"""
237 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted', 'completed'])
237 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted', 'completed'])
238 for rec in found:
238 for rec in found:
239 self.assertEqual(set(rec.keys()), set(['msg_id', 'submitted', 'completed']))
239 self.assertEqual(set(rec.keys()), set(['msg_id', 'submitted', 'completed']))
240
240
241 def test_db_query_default_keys(self):
241 def test_db_query_default_keys(self):
242 """default db_query excludes buffers"""
242 """default db_query excludes buffers"""
243 found = self.client.db_query({'msg_id': {'$ne' : ''}})
243 found = self.client.db_query({'msg_id': {'$ne' : ''}})
244 for rec in found:
244 for rec in found:
245 keys = set(rec.keys())
245 keys = set(rec.keys())
246 self.assertFalse('buffers' in keys, "'buffers' should not be in: %s" % keys)
246 self.assertFalse('buffers' in keys, "'buffers' should not be in: %s" % keys)
247 self.assertFalse('result_buffers' in keys, "'result_buffers' should not be in: %s" % keys)
247 self.assertFalse('result_buffers' in keys, "'result_buffers' should not be in: %s" % keys)
248
248
249 def test_db_query_msg_id(self):
249 def test_db_query_msg_id(self):
250 """ensure msg_id is always in db queries"""
250 """ensure msg_id is always in db queries"""
251 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted', 'completed'])
251 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted', 'completed'])
252 for rec in found:
252 for rec in found:
253 self.assertTrue('msg_id' in rec.keys())
253 self.assertTrue('msg_id' in rec.keys())
254 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted'])
254 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['submitted'])
255 for rec in found:
255 for rec in found:
256 self.assertTrue('msg_id' in rec.keys())
256 self.assertTrue('msg_id' in rec.keys())
257 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['msg_id'])
257 found = self.client.db_query({'msg_id': {'$ne' : ''}},keys=['msg_id'])
258 for rec in found:
258 for rec in found:
259 self.assertTrue('msg_id' in rec.keys())
259 self.assertTrue('msg_id' in rec.keys())
260
260
261 def test_db_query_get_result(self):
261 def test_db_query_get_result(self):
262 """pop in db_query shouldn't pop from result itself"""
262 """pop in db_query shouldn't pop from result itself"""
263 self.client[:].apply_sync(lambda : 1)
263 self.client[:].apply_sync(lambda : 1)
264 found = self.client.db_query({'msg_id': {'$ne' : ''}})
264 found = self.client.db_query({'msg_id': {'$ne' : ''}})
265 rc2 = clientmod.Client(profile='iptest')
265 rc2 = clientmod.Client(profile='iptest')
266 # If this bug is not fixed, this call will hang:
266 # If this bug is not fixed, this call will hang:
267 ar = rc2.get_result(self.client.history[-1])
267 ar = rc2.get_result(self.client.history[-1])
268 ar.wait(2)
268 ar.wait(2)
269 self.assertTrue(ar.ready())
269 self.assertTrue(ar.ready())
270 ar.get()
270 ar.get()
271 rc2.close()
271 rc2.close()
272
272
273 def test_db_query_in(self):
273 def test_db_query_in(self):
274 """test db query with '$in','$nin' operators"""
274 """test db query with '$in','$nin' operators"""
275 hist = self.client.hub_history()
275 hist = self.client.hub_history()
276 even = hist[::2]
276 even = hist[::2]
277 odd = hist[1::2]
277 odd = hist[1::2]
278 recs = self.client.db_query({ 'msg_id' : {'$in' : even}})
278 recs = self.client.db_query({ 'msg_id' : {'$in' : even}})
279 found = [ r['msg_id'] for r in recs ]
279 found = [ r['msg_id'] for r in recs ]
280 self.assertEqual(set(even), set(found))
280 self.assertEqual(set(even), set(found))
281 recs = self.client.db_query({ 'msg_id' : {'$nin' : even}})
281 recs = self.client.db_query({ 'msg_id' : {'$nin' : even}})
282 found = [ r['msg_id'] for r in recs ]
282 found = [ r['msg_id'] for r in recs ]
283 self.assertEqual(set(odd), set(found))
283 self.assertEqual(set(odd), set(found))
284
284
285 def test_hub_history(self):
285 def test_hub_history(self):
286 hist = self.client.hub_history()
286 hist = self.client.hub_history()
287 recs = self.client.db_query({ 'msg_id' : {"$ne":''}})
287 recs = self.client.db_query({ 'msg_id' : {"$ne":''}})
288 recdict = {}
288 recdict = {}
289 for rec in recs:
289 for rec in recs:
290 recdict[rec['msg_id']] = rec
290 recdict[rec['msg_id']] = rec
291
291
292 latest = datetime(1984,1,1)
292 latest = datetime(1984,1,1)
293 for msg_id in hist:
293 for msg_id in hist:
294 rec = recdict[msg_id]
294 rec = recdict[msg_id]
295 newt = rec['submitted']
295 newt = rec['submitted']
296 self.assertTrue(newt >= latest)
296 self.assertTrue(newt >= latest)
297 latest = newt
297 latest = newt
298 ar = self.client[-1].apply_async(lambda : 1)
298 ar = self.client[-1].apply_async(lambda : 1)
299 ar.get()
299 ar.get()
300 time.sleep(0.25)
300 time.sleep(0.25)
301 self.assertEqual(self.client.hub_history()[-1:],ar.msg_ids)
301 self.assertEqual(self.client.hub_history()[-1:],ar.msg_ids)
302
302
303 def _wait_for_idle(self):
303 def _wait_for_idle(self):
304 """wait for an engine to become idle, according to the Hub"""
304 """wait for an engine to become idle, according to the Hub"""
305 rc = self.client
305 rc = self.client
306
306
307 # step 1. wait for all requests to be noticed
307 # step 1. wait for all requests to be noticed
308 # timeout 5s, polling every 100ms
308 # timeout 5s, polling every 100ms
309 msg_ids = set(rc.history)
309 msg_ids = set(rc.history)
310 hub_hist = rc.hub_history()
310 hub_hist = rc.hub_history()
311 for i in range(50):
311 for i in range(50):
312 if msg_ids.difference(hub_hist):
312 if msg_ids.difference(hub_hist):
313 time.sleep(0.1)
313 time.sleep(0.1)
314 hub_hist = rc.hub_history()
314 hub_hist = rc.hub_history()
315 else:
315 else:
316 break
316 break
317
317
318 self.assertEqual(len(msg_ids.difference(hub_hist)), 0)
318 self.assertEqual(len(msg_ids.difference(hub_hist)), 0)
319
319
320 # step 2. wait for all requests to be done
320 # step 2. wait for all requests to be done
321 # timeout 5s, polling every 100ms
321 # timeout 5s, polling every 100ms
322 qs = rc.queue_status()
322 qs = rc.queue_status()
323 for i in range(50):
323 for i in range(50):
324 if qs['unassigned'] or any(qs[eid]['tasks'] for eid in rc.ids):
324 if qs['unassigned'] or any(qs[eid]['tasks'] for eid in rc.ids):
325 time.sleep(0.1)
325 time.sleep(0.1)
326 qs = rc.queue_status()
326 qs = rc.queue_status()
327 else:
327 else:
328 break
328 break
329
329
330 # ensure Hub up to date:
330 # ensure Hub up to date:
331 self.assertEqual(qs['unassigned'], 0)
331 self.assertEqual(qs['unassigned'], 0)
332 for eid in rc.ids:
332 for eid in rc.ids:
333 self.assertEqual(qs[eid]['tasks'], 0)
333 self.assertEqual(qs[eid]['tasks'], 0)
334
334
335
335
336 def test_resubmit(self):
336 def test_resubmit(self):
337 def f():
337 def f():
338 import random
338 import random
339 return random.random()
339 return random.random()
340 v = self.client.load_balanced_view()
340 v = self.client.load_balanced_view()
341 ar = v.apply_async(f)
341 ar = v.apply_async(f)
342 r1 = ar.get(1)
342 r1 = ar.get(1)
343 # give the Hub a chance to notice:
343 # give the Hub a chance to notice:
344 self._wait_for_idle()
344 self._wait_for_idle()
345 ahr = self.client.resubmit(ar.msg_ids)
345 ahr = self.client.resubmit(ar.msg_ids)
346 r2 = ahr.get(1)
346 r2 = ahr.get(1)
347 self.assertFalse(r1 == r2)
347 self.assertFalse(r1 == r2)
348
348
349 def test_resubmit_chain(self):
349 def test_resubmit_chain(self):
350 """resubmit resubmitted tasks"""
350 """resubmit resubmitted tasks"""
351 v = self.client.load_balanced_view()
351 v = self.client.load_balanced_view()
352 ar = v.apply_async(lambda x: x, 'x'*1024)
352 ar = v.apply_async(lambda x: x, 'x'*1024)
353 ar.get()
353 ar.get()
354 self._wait_for_idle()
354 self._wait_for_idle()
355 ars = [ar]
355 ars = [ar]
356
356
357 for i in range(10):
357 for i in range(10):
358 ar = ars[-1]
358 ar = ars[-1]
359 ar2 = self.client.resubmit(ar.msg_ids)
359 ar2 = self.client.resubmit(ar.msg_ids)
360
360
361 [ ar.get() for ar in ars ]
361 [ ar.get() for ar in ars ]
362
362
363 def test_resubmit_header(self):
363 def test_resubmit_header(self):
364 """resubmit shouldn't clobber the whole header"""
364 """resubmit shouldn't clobber the whole header"""
365 def f():
365 def f():
366 import random
366 import random
367 return random.random()
367 return random.random()
368 v = self.client.load_balanced_view()
368 v = self.client.load_balanced_view()
369 v.retries = 1
369 v.retries = 1
370 ar = v.apply_async(f)
370 ar = v.apply_async(f)
371 r1 = ar.get(1)
371 r1 = ar.get(1)
372 # give the Hub a chance to notice:
372 # give the Hub a chance to notice:
373 self._wait_for_idle()
373 self._wait_for_idle()
374 ahr = self.client.resubmit(ar.msg_ids)
374 ahr = self.client.resubmit(ar.msg_ids)
375 ahr.get(1)
375 ahr.get(1)
376 time.sleep(0.5)
376 time.sleep(0.5)
377 records = self.client.db_query({'msg_id': {'$in': ar.msg_ids + ahr.msg_ids}}, keys='header')
377 records = self.client.db_query({'msg_id': {'$in': ar.msg_ids + ahr.msg_ids}}, keys='header')
378 h1,h2 = [ r['header'] for r in records ]
378 h1,h2 = [ r['header'] for r in records ]
379 for key in set(h1.keys()).union(set(h2.keys())):
379 for key in set(h1.keys()).union(set(h2.keys())):
380 if key in ('msg_id', 'date'):
380 if key in ('msg_id', 'date'):
381 self.assertNotEqual(h1[key], h2[key])
381 self.assertNotEqual(h1[key], h2[key])
382 else:
382 else:
383 self.assertEqual(h1[key], h2[key])
383 self.assertEqual(h1[key], h2[key])
384
384
385 def test_resubmit_aborted(self):
385 def test_resubmit_aborted(self):
386 def f():
386 def f():
387 import random
387 import random
388 return random.random()
388 return random.random()
389 v = self.client.load_balanced_view()
389 v = self.client.load_balanced_view()
390 # restrict to one engine, so we can put a sleep
390 # restrict to one engine, so we can put a sleep
391 # ahead of the task, so it will get aborted
391 # ahead of the task, so it will get aborted
392 eid = self.client.ids[-1]
392 eid = self.client.ids[-1]
393 v.targets = [eid]
393 v.targets = [eid]
394 sleep = v.apply_async(time.sleep, 0.5)
394 sleep = v.apply_async(time.sleep, 0.5)
395 ar = v.apply_async(f)
395 ar = v.apply_async(f)
396 ar.abort()
396 ar.abort()
397 self.assertRaises(error.TaskAborted, ar.get)
397 self.assertRaises(error.TaskAborted, ar.get)
398 # Give the Hub a chance to get up to date:
398 # Give the Hub a chance to get up to date:
399 self._wait_for_idle()
399 self._wait_for_idle()
400 ahr = self.client.resubmit(ar.msg_ids)
400 ahr = self.client.resubmit(ar.msg_ids)
401 r2 = ahr.get(1)
401 r2 = ahr.get(1)
402
402
403 def test_resubmit_inflight(self):
403 def test_resubmit_inflight(self):
404 """resubmit of inflight task"""
404 """resubmit of inflight task"""
405 v = self.client.load_balanced_view()
405 v = self.client.load_balanced_view()
406 ar = v.apply_async(time.sleep,1)
406 ar = v.apply_async(time.sleep,1)
407 # give the message a chance to arrive
407 # give the message a chance to arrive
408 time.sleep(0.2)
408 time.sleep(0.2)
409 ahr = self.client.resubmit(ar.msg_ids)
409 ahr = self.client.resubmit(ar.msg_ids)
410 ar.get(2)
410 ar.get(2)
411 ahr.get(2)
411 ahr.get(2)
412
412
413 def test_resubmit_badkey(self):
413 def test_resubmit_badkey(self):
414 """ensure KeyError on resubmit of nonexistant task"""
414 """ensure KeyError on resubmit of nonexistant task"""
415 self.assertRaisesRemote(KeyError, self.client.resubmit, ['invalid'])
415 self.assertRaisesRemote(KeyError, self.client.resubmit, ['invalid'])
416
416
417 def test_purge_hub_results(self):
417 def test_purge_hub_results(self):
418 # ensure there are some tasks
418 # ensure there are some tasks
419 for i in range(5):
419 for i in range(5):
420 self.client[:].apply_sync(lambda : 1)
420 self.client[:].apply_sync(lambda : 1)
421 # Wait for the Hub to realise the result is done:
421 # Wait for the Hub to realise the result is done:
422 # This prevents a race condition, where we
422 # This prevents a race condition, where we
423 # might purge a result the Hub still thinks is pending.
423 # might purge a result the Hub still thinks is pending.
424 self._wait_for_idle()
424 self._wait_for_idle()
425 rc2 = clientmod.Client(profile='iptest')
425 rc2 = clientmod.Client(profile='iptest')
426 hist = self.client.hub_history()
426 hist = self.client.hub_history()
427 ahr = rc2.get_result([hist[-1]])
427 ahr = rc2.get_result([hist[-1]])
428 ahr.wait(10)
428 ahr.wait(10)
429 self.client.purge_hub_results(hist[-1])
429 self.client.purge_hub_results(hist[-1])
430 newhist = self.client.hub_history()
430 newhist = self.client.hub_history()
431 self.assertEqual(len(newhist)+1,len(hist))
431 self.assertEqual(len(newhist)+1,len(hist))
432 rc2.spin()
432 rc2.spin()
433 rc2.close()
433 rc2.close()
434
434
435 def test_purge_local_results(self):
435 def test_purge_local_results(self):
436 # ensure there are some tasks
436 # ensure there are some tasks
437 res = []
437 res = []
438 for i in range(5):
438 for i in range(5):
439 res.append(self.client[:].apply_async(lambda : 1))
439 res.append(self.client[:].apply_async(lambda : 1))
440 self._wait_for_idle()
440 self._wait_for_idle()
441 self.client.wait(10) # wait for the results to come back
441 self.client.wait(10) # wait for the results to come back
442 before = len(self.client.results)
442 before = len(self.client.results)
443 self.assertEqual(len(self.client.metadata),before)
443 self.assertEqual(len(self.client.metadata),before)
444 self.client.purge_local_results(res[-1])
444 self.client.purge_local_results(res[-1])
445 self.assertEqual(len(self.client.results),before-len(res[-1]), msg="Not removed from results")
445 self.assertEqual(len(self.client.results),before-len(res[-1]), msg="Not removed from results")
446 self.assertEqual(len(self.client.metadata),before-len(res[-1]), msg="Not removed from metadata")
446 self.assertEqual(len(self.client.metadata),before-len(res[-1]), msg="Not removed from metadata")
447
447
448 def test_purge_all_hub_results(self):
448 def test_purge_all_hub_results(self):
449 self.client.purge_hub_results('all')
449 self.client.purge_hub_results('all')
450 hist = self.client.hub_history()
450 hist = self.client.hub_history()
451 self.assertEqual(len(hist), 0)
451 self.assertEqual(len(hist), 0)
452
452
453 def test_purge_all_local_results(self):
453 def test_purge_all_local_results(self):
454 self.client.purge_local_results('all')
454 self.client.purge_local_results('all')
455 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
455 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
456 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
456 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
457
457
458 def test_purge_all_results(self):
458 def test_purge_all_results(self):
459 # ensure there are some tasks
459 # ensure there are some tasks
460 for i in range(5):
460 for i in range(5):
461 self.client[:].apply_sync(lambda : 1)
461 self.client[:].apply_sync(lambda : 1)
462 self.client.wait(10)
462 self.client.wait(10)
463 self._wait_for_idle()
463 self._wait_for_idle()
464 self.client.purge_results('all')
464 self.client.purge_results('all')
465 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
465 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
466 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
466 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
467 hist = self.client.hub_history()
467 hist = self.client.hub_history()
468 self.assertEqual(len(hist), 0, msg="hub history not empty")
468 self.assertEqual(len(hist), 0, msg="hub history not empty")
469
469
470 def test_purge_everything(self):
470 def test_purge_everything(self):
471 # ensure there are some tasks
471 # ensure there are some tasks
472 for i in range(5):
472 for i in range(5):
473 self.client[:].apply_sync(lambda : 1)
473 self.client[:].apply_sync(lambda : 1)
474 self.client.wait(10)
474 self.client.wait(10)
475 self._wait_for_idle()
475 self._wait_for_idle()
476 self.client.purge_everything()
476 self.client.purge_everything()
477 # The client results
477 # The client results
478 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
478 self.assertEqual(len(self.client.results), 0, msg="Results not empty")
479 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
479 self.assertEqual(len(self.client.metadata), 0, msg="metadata not empty")
480 # The client "bookkeeping"
480 # The client "bookkeeping"
481 self.assertEqual(len(self.client.session.digest_history), 0, msg="session digest not empty")
481 self.assertEqual(len(self.client.session.digest_history), 0, msg="session digest not empty")
482 self.assertEqual(len(self.client.history), 0, msg="client history not empty")
482 self.assertEqual(len(self.client.history), 0, msg="client history not empty")
483 # the hub results
483 # the hub results
484 hist = self.client.hub_history()
484 hist = self.client.hub_history()
485 self.assertEqual(len(hist), 0, msg="hub history not empty")
485 self.assertEqual(len(hist), 0, msg="hub history not empty")
486
486
487
487
488 def test_spin_thread(self):
488 def test_spin_thread(self):
489 self.client.spin_thread(0.01)
489 self.client.spin_thread(0.01)
490 ar = self.client[-1].apply_async(lambda : 1)
490 ar = self.client[-1].apply_async(lambda : 1)
491 time.sleep(0.1)
491 time.sleep(0.1)
492 self.assertTrue(ar.wall_time < 0.1,
492 self.assertTrue(ar.wall_time < 0.1,
493 "spin should have kept wall_time < 0.1, but got %f" % ar.wall_time
493 "spin should have kept wall_time < 0.1, but got %f" % ar.wall_time
494 )
494 )
495
495
496 def test_stop_spin_thread(self):
496 def test_stop_spin_thread(self):
497 self.client.spin_thread(0.01)
497 self.client.spin_thread(0.01)
498 self.client.stop_spin_thread()
498 self.client.stop_spin_thread()
499 ar = self.client[-1].apply_async(lambda : 1)
499 ar = self.client[-1].apply_async(lambda : 1)
500 time.sleep(0.15)
500 time.sleep(0.15)
501 self.assertTrue(ar.wall_time > 0.1,
501 self.assertTrue(ar.wall_time > 0.1,
502 "Shouldn't be spinning, but got wall_time=%f" % ar.wall_time
502 "Shouldn't be spinning, but got wall_time=%f" % ar.wall_time
503 )
503 )
504
504
505 def test_activate(self):
505 def test_activate(self):
506 ip = get_ipython()
506 ip = get_ipython()
507 magics = ip.magics_manager.magics
507 magics = ip.magics_manager.magics
508 self.assertTrue('px' in magics['line'])
508 self.assertTrue('px' in magics['line'])
509 self.assertTrue('px' in magics['cell'])
509 self.assertTrue('px' in magics['cell'])
510 v0 = self.client.activate(-1, '0')
510 v0 = self.client.activate(-1, '0')
511 self.assertTrue('px0' in magics['line'])
511 self.assertTrue('px0' in magics['line'])
512 self.assertTrue('px0' in magics['cell'])
512 self.assertTrue('px0' in magics['cell'])
513 self.assertEqual(v0.targets, self.client.ids[-1])
513 self.assertEqual(v0.targets, self.client.ids[-1])
514 v0 = self.client.activate('all', 'all')
514 v0 = self.client.activate('all', 'all')
515 self.assertTrue('pxall' in magics['line'])
515 self.assertTrue('pxall' in magics['line'])
516 self.assertTrue('pxall' in magics['cell'])
516 self.assertTrue('pxall' in magics['cell'])
517 self.assertEqual(v0.targets, 'all')
517 self.assertEqual(v0.targets, 'all')
@@ -1,2110 +1,2110 b''
1 """ An abstract base class for console-type widgets.
1 """ An abstract base class for console-type widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 import os.path
8 import os.path
9 import re
9 import re
10 import sys
10 import sys
11 from textwrap import dedent
11 from textwrap import dedent
12 import time
12 import time
13 from unicodedata import category
13 from unicodedata import category
14 import webbrowser
14 import webbrowser
15
15
16 # System library imports
16 # System library imports
17 from IPython.external.qt import QtCore, QtGui
17 from IPython.external.qt import QtCore, QtGui
18
18
19 # Local imports
19 # Local imports
20 from IPython.config.configurable import LoggingConfigurable
20 from IPython.config.configurable import LoggingConfigurable
21 from IPython.core.inputsplitter import ESC_SEQUENCES
21 from IPython.core.inputsplitter import ESC_SEQUENCES
22 from IPython.qt.rich_text import HtmlExporter
22 from IPython.qt.rich_text import HtmlExporter
23 from IPython.qt.util import MetaQObjectHasTraits, get_font
23 from IPython.qt.util import MetaQObjectHasTraits, get_font
24 from IPython.utils.text import columnize
24 from IPython.utils.text import columnize
25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
26 from ansi_code_processor import QtAnsiCodeProcessor
26 from .ansi_code_processor import QtAnsiCodeProcessor
27 from completion_widget import CompletionWidget
27 from .completion_widget import CompletionWidget
28 from completion_html import CompletionHtml
28 from .completion_html import CompletionHtml
29 from completion_plain import CompletionPlain
29 from .completion_plain import CompletionPlain
30 from kill_ring import QtKillRing
30 from .kill_ring import QtKillRing
31
31
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Functions
34 # Functions
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
39
39
40 def commonprefix(items):
40 def commonprefix(items):
41 """Get common prefix for completions
41 """Get common prefix for completions
42
42
43 Return the longest common prefix of a list of strings, but with special
43 Return the longest common prefix of a list of strings, but with special
44 treatment of escape characters that might precede commands in IPython,
44 treatment of escape characters that might precede commands in IPython,
45 such as %magic functions. Used in tab completion.
45 such as %magic functions. Used in tab completion.
46
46
47 For a more general function, see os.path.commonprefix
47 For a more general function, see os.path.commonprefix
48 """
48 """
49 # the last item will always have the least leading % symbol
49 # the last item will always have the least leading % symbol
50 # min / max are first/last in alphabetical order
50 # min / max are first/last in alphabetical order
51 first_match = ESCAPE_RE.match(min(items))
51 first_match = ESCAPE_RE.match(min(items))
52 last_match = ESCAPE_RE.match(max(items))
52 last_match = ESCAPE_RE.match(max(items))
53 # common suffix is (common prefix of reversed items) reversed
53 # common suffix is (common prefix of reversed items) reversed
54 if first_match and last_match:
54 if first_match and last_match:
55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
56 else:
56 else:
57 prefix = ''
57 prefix = ''
58
58
59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
60 return prefix+os.path.commonprefix(items)
60 return prefix+os.path.commonprefix(items)
61
61
62 def is_letter_or_number(char):
62 def is_letter_or_number(char):
63 """ Returns whether the specified unicode character is a letter or a number.
63 """ Returns whether the specified unicode character is a letter or a number.
64 """
64 """
65 cat = category(char)
65 cat = category(char)
66 return cat.startswith('L') or cat.startswith('N')
66 return cat.startswith('L') or cat.startswith('N')
67
67
68 #-----------------------------------------------------------------------------
68 #-----------------------------------------------------------------------------
69 # Classes
69 # Classes
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71
71
72 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
72 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
73 """ An abstract base class for console-type widgets. This class has
73 """ An abstract base class for console-type widgets. This class has
74 functionality for:
74 functionality for:
75
75
76 * Maintaining a prompt and editing region
76 * Maintaining a prompt and editing region
77 * Providing the traditional Unix-style console keyboard shortcuts
77 * Providing the traditional Unix-style console keyboard shortcuts
78 * Performing tab completion
78 * Performing tab completion
79 * Paging text
79 * Paging text
80 * Handling ANSI escape codes
80 * Handling ANSI escape codes
81
81
82 ConsoleWidget also provides a number of utility methods that will be
82 ConsoleWidget also provides a number of utility methods that will be
83 convenient to implementors of a console-style widget.
83 convenient to implementors of a console-style widget.
84 """
84 """
85 __metaclass__ = MetaQObjectHasTraits
85 __metaclass__ = MetaQObjectHasTraits
86
86
87 #------ Configuration ------------------------------------------------------
87 #------ Configuration ------------------------------------------------------
88
88
89 ansi_codes = Bool(True, config=True,
89 ansi_codes = Bool(True, config=True,
90 help="Whether to process ANSI escape codes."
90 help="Whether to process ANSI escape codes."
91 )
91 )
92 buffer_size = Integer(500, config=True,
92 buffer_size = Integer(500, config=True,
93 help="""
93 help="""
94 The maximum number of lines of text before truncation. Specifying a
94 The maximum number of lines of text before truncation. Specifying a
95 non-positive number disables text truncation (not recommended).
95 non-positive number disables text truncation (not recommended).
96 """
96 """
97 )
97 )
98 execute_on_complete_input = Bool(True, config=True,
98 execute_on_complete_input = Bool(True, config=True,
99 help="""Whether to automatically execute on syntactically complete input.
99 help="""Whether to automatically execute on syntactically complete input.
100
100
101 If False, Shift-Enter is required to submit each execution.
101 If False, Shift-Enter is required to submit each execution.
102 Disabling this is mainly useful for non-Python kernels,
102 Disabling this is mainly useful for non-Python kernels,
103 where the completion check would be wrong.
103 where the completion check would be wrong.
104 """
104 """
105 )
105 )
106 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
106 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
107 default_value = 'ncurses',
107 default_value = 'ncurses',
108 help="""
108 help="""
109 The type of completer to use. Valid values are:
109 The type of completer to use. Valid values are:
110
110
111 'plain' : Show the available completion as a text list
111 'plain' : Show the available completion as a text list
112 Below the editing area.
112 Below the editing area.
113 'droplist': Show the completion in a drop down list navigable
113 'droplist': Show the completion in a drop down list navigable
114 by the arrow keys, and from which you can select
114 by the arrow keys, and from which you can select
115 completion by pressing Return.
115 completion by pressing Return.
116 'ncurses' : Show the completion as a text list which is navigable by
116 'ncurses' : Show the completion as a text list which is navigable by
117 `tab` and arrow keys.
117 `tab` and arrow keys.
118 """
118 """
119 )
119 )
120 # NOTE: this value can only be specified during initialization.
120 # NOTE: this value can only be specified during initialization.
121 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
121 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
122 help="""
122 help="""
123 The type of underlying text widget to use. Valid values are 'plain',
123 The type of underlying text widget to use. Valid values are 'plain',
124 which specifies a QPlainTextEdit, and 'rich', which specifies a
124 which specifies a QPlainTextEdit, and 'rich', which specifies a
125 QTextEdit.
125 QTextEdit.
126 """
126 """
127 )
127 )
128 # NOTE: this value can only be specified during initialization.
128 # NOTE: this value can only be specified during initialization.
129 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
129 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
130 default_value='inside', config=True,
130 default_value='inside', config=True,
131 help="""
131 help="""
132 The type of paging to use. Valid values are:
132 The type of paging to use. Valid values are:
133
133
134 'inside' : The widget pages like a traditional terminal.
134 'inside' : The widget pages like a traditional terminal.
135 'hsplit' : When paging is requested, the widget is split
135 'hsplit' : When paging is requested, the widget is split
136 horizontally. The top pane contains the console, and the
136 horizontally. The top pane contains the console, and the
137 bottom pane contains the paged text.
137 bottom pane contains the paged text.
138 'vsplit' : Similar to 'hsplit', except that a vertical splitter
138 'vsplit' : Similar to 'hsplit', except that a vertical splitter
139 used.
139 used.
140 'custom' : No action is taken by the widget beyond emitting a
140 'custom' : No action is taken by the widget beyond emitting a
141 'custom_page_requested(str)' signal.
141 'custom_page_requested(str)' signal.
142 'none' : The text is written directly to the console.
142 'none' : The text is written directly to the console.
143 """)
143 """)
144
144
145 font_family = Unicode(config=True,
145 font_family = Unicode(config=True,
146 help="""The font family to use for the console.
146 help="""The font family to use for the console.
147 On OSX this defaults to Monaco, on Windows the default is
147 On OSX this defaults to Monaco, on Windows the default is
148 Consolas with fallback of Courier, and on other platforms
148 Consolas with fallback of Courier, and on other platforms
149 the default is Monospace.
149 the default is Monospace.
150 """)
150 """)
151 def _font_family_default(self):
151 def _font_family_default(self):
152 if sys.platform == 'win32':
152 if sys.platform == 'win32':
153 # Consolas ships with Vista/Win7, fallback to Courier if needed
153 # Consolas ships with Vista/Win7, fallback to Courier if needed
154 return 'Consolas'
154 return 'Consolas'
155 elif sys.platform == 'darwin':
155 elif sys.platform == 'darwin':
156 # OSX always has Monaco, no need for a fallback
156 # OSX always has Monaco, no need for a fallback
157 return 'Monaco'
157 return 'Monaco'
158 else:
158 else:
159 # Monospace should always exist, no need for a fallback
159 # Monospace should always exist, no need for a fallback
160 return 'Monospace'
160 return 'Monospace'
161
161
162 font_size = Integer(config=True,
162 font_size = Integer(config=True,
163 help="""The font size. If unconfigured, Qt will be entrusted
163 help="""The font size. If unconfigured, Qt will be entrusted
164 with the size of the font.
164 with the size of the font.
165 """)
165 """)
166
166
167 width = Integer(81, config=True,
167 width = Integer(81, config=True,
168 help="""The width of the console at start time in number
168 help="""The width of the console at start time in number
169 of characters (will double with `hsplit` paging)
169 of characters (will double with `hsplit` paging)
170 """)
170 """)
171
171
172 height = Integer(25, config=True,
172 height = Integer(25, config=True,
173 help="""The height of the console at start time in number
173 help="""The height of the console at start time in number
174 of characters (will double with `vsplit` paging)
174 of characters (will double with `vsplit` paging)
175 """)
175 """)
176
176
177 # Whether to override ShortcutEvents for the keybindings defined by this
177 # Whether to override ShortcutEvents for the keybindings defined by this
178 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
178 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
179 # priority (when it has focus) over, e.g., window-level menu shortcuts.
179 # priority (when it has focus) over, e.g., window-level menu shortcuts.
180 override_shortcuts = Bool(False)
180 override_shortcuts = Bool(False)
181
181
182 # ------ Custom Qt Widgets -------------------------------------------------
182 # ------ Custom Qt Widgets -------------------------------------------------
183
183
184 # For other projects to easily override the Qt widgets used by the console
184 # For other projects to easily override the Qt widgets used by the console
185 # (e.g. Spyder)
185 # (e.g. Spyder)
186 custom_control = None
186 custom_control = None
187 custom_page_control = None
187 custom_page_control = None
188
188
189 #------ Signals ------------------------------------------------------------
189 #------ Signals ------------------------------------------------------------
190
190
191 # Signals that indicate ConsoleWidget state.
191 # Signals that indicate ConsoleWidget state.
192 copy_available = QtCore.Signal(bool)
192 copy_available = QtCore.Signal(bool)
193 redo_available = QtCore.Signal(bool)
193 redo_available = QtCore.Signal(bool)
194 undo_available = QtCore.Signal(bool)
194 undo_available = QtCore.Signal(bool)
195
195
196 # Signal emitted when paging is needed and the paging style has been
196 # Signal emitted when paging is needed and the paging style has been
197 # specified as 'custom'.
197 # specified as 'custom'.
198 custom_page_requested = QtCore.Signal(object)
198 custom_page_requested = QtCore.Signal(object)
199
199
200 # Signal emitted when the font is changed.
200 # Signal emitted when the font is changed.
201 font_changed = QtCore.Signal(QtGui.QFont)
201 font_changed = QtCore.Signal(QtGui.QFont)
202
202
203 #------ Protected class variables ------------------------------------------
203 #------ Protected class variables ------------------------------------------
204
204
205 # control handles
205 # control handles
206 _control = None
206 _control = None
207 _page_control = None
207 _page_control = None
208 _splitter = None
208 _splitter = None
209
209
210 # When the control key is down, these keys are mapped.
210 # When the control key is down, these keys are mapped.
211 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
211 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
212 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
212 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
213 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
213 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
214 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
214 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
215 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
215 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
216 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
216 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
217 if not sys.platform == 'darwin':
217 if not sys.platform == 'darwin':
218 # On OS X, Ctrl-E already does the right thing, whereas End moves the
218 # On OS X, Ctrl-E already does the right thing, whereas End moves the
219 # cursor to the bottom of the buffer.
219 # cursor to the bottom of the buffer.
220 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
220 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
221
221
222 # The shortcuts defined by this widget. We need to keep track of these to
222 # The shortcuts defined by this widget. We need to keep track of these to
223 # support 'override_shortcuts' above.
223 # support 'override_shortcuts' above.
224 _shortcuts = set(_ctrl_down_remap.keys() +
224 _shortcuts = set(_ctrl_down_remap.keys() +
225 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
225 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
226 QtCore.Qt.Key_V ])
226 QtCore.Qt.Key_V ])
227
227
228 _temp_buffer_filled = False
228 _temp_buffer_filled = False
229
229
230 #---------------------------------------------------------------------------
230 #---------------------------------------------------------------------------
231 # 'QObject' interface
231 # 'QObject' interface
232 #---------------------------------------------------------------------------
232 #---------------------------------------------------------------------------
233
233
234 def __init__(self, parent=None, **kw):
234 def __init__(self, parent=None, **kw):
235 """ Create a ConsoleWidget.
235 """ Create a ConsoleWidget.
236
236
237 Parameters:
237 Parameters:
238 -----------
238 -----------
239 parent : QWidget, optional [default None]
239 parent : QWidget, optional [default None]
240 The parent for this widget.
240 The parent for this widget.
241 """
241 """
242 QtGui.QWidget.__init__(self, parent)
242 QtGui.QWidget.__init__(self, parent)
243 LoggingConfigurable.__init__(self, **kw)
243 LoggingConfigurable.__init__(self, **kw)
244
244
245 # While scrolling the pager on Mac OS X, it tears badly. The
245 # While scrolling the pager on Mac OS X, it tears badly. The
246 # NativeGesture is platform and perhaps build-specific hence
246 # NativeGesture is platform and perhaps build-specific hence
247 # we take adequate precautions here.
247 # we take adequate precautions here.
248 self._pager_scroll_events = [QtCore.QEvent.Wheel]
248 self._pager_scroll_events = [QtCore.QEvent.Wheel]
249 if hasattr(QtCore.QEvent, 'NativeGesture'):
249 if hasattr(QtCore.QEvent, 'NativeGesture'):
250 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
250 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
251
251
252 # Create the layout and underlying text widget.
252 # Create the layout and underlying text widget.
253 layout = QtGui.QStackedLayout(self)
253 layout = QtGui.QStackedLayout(self)
254 layout.setContentsMargins(0, 0, 0, 0)
254 layout.setContentsMargins(0, 0, 0, 0)
255 self._control = self._create_control()
255 self._control = self._create_control()
256 if self.paging in ('hsplit', 'vsplit'):
256 if self.paging in ('hsplit', 'vsplit'):
257 self._splitter = QtGui.QSplitter()
257 self._splitter = QtGui.QSplitter()
258 if self.paging == 'hsplit':
258 if self.paging == 'hsplit':
259 self._splitter.setOrientation(QtCore.Qt.Horizontal)
259 self._splitter.setOrientation(QtCore.Qt.Horizontal)
260 else:
260 else:
261 self._splitter.setOrientation(QtCore.Qt.Vertical)
261 self._splitter.setOrientation(QtCore.Qt.Vertical)
262 self._splitter.addWidget(self._control)
262 self._splitter.addWidget(self._control)
263 layout.addWidget(self._splitter)
263 layout.addWidget(self._splitter)
264 else:
264 else:
265 layout.addWidget(self._control)
265 layout.addWidget(self._control)
266
266
267 # Create the paging widget, if necessary.
267 # Create the paging widget, if necessary.
268 if self.paging in ('inside', 'hsplit', 'vsplit'):
268 if self.paging in ('inside', 'hsplit', 'vsplit'):
269 self._page_control = self._create_page_control()
269 self._page_control = self._create_page_control()
270 if self._splitter:
270 if self._splitter:
271 self._page_control.hide()
271 self._page_control.hide()
272 self._splitter.addWidget(self._page_control)
272 self._splitter.addWidget(self._page_control)
273 else:
273 else:
274 layout.addWidget(self._page_control)
274 layout.addWidget(self._page_control)
275
275
276 # Initialize protected variables. Some variables contain useful state
276 # Initialize protected variables. Some variables contain useful state
277 # information for subclasses; they should be considered read-only.
277 # information for subclasses; they should be considered read-only.
278 self._append_before_prompt_pos = 0
278 self._append_before_prompt_pos = 0
279 self._ansi_processor = QtAnsiCodeProcessor()
279 self._ansi_processor = QtAnsiCodeProcessor()
280 if self.gui_completion == 'ncurses':
280 if self.gui_completion == 'ncurses':
281 self._completion_widget = CompletionHtml(self)
281 self._completion_widget = CompletionHtml(self)
282 elif self.gui_completion == 'droplist':
282 elif self.gui_completion == 'droplist':
283 self._completion_widget = CompletionWidget(self)
283 self._completion_widget = CompletionWidget(self)
284 elif self.gui_completion == 'plain':
284 elif self.gui_completion == 'plain':
285 self._completion_widget = CompletionPlain(self)
285 self._completion_widget = CompletionPlain(self)
286
286
287 self._continuation_prompt = '> '
287 self._continuation_prompt = '> '
288 self._continuation_prompt_html = None
288 self._continuation_prompt_html = None
289 self._executing = False
289 self._executing = False
290 self._filter_resize = False
290 self._filter_resize = False
291 self._html_exporter = HtmlExporter(self._control)
291 self._html_exporter = HtmlExporter(self._control)
292 self._input_buffer_executing = ''
292 self._input_buffer_executing = ''
293 self._input_buffer_pending = ''
293 self._input_buffer_pending = ''
294 self._kill_ring = QtKillRing(self._control)
294 self._kill_ring = QtKillRing(self._control)
295 self._prompt = ''
295 self._prompt = ''
296 self._prompt_html = None
296 self._prompt_html = None
297 self._prompt_pos = 0
297 self._prompt_pos = 0
298 self._prompt_sep = ''
298 self._prompt_sep = ''
299 self._reading = False
299 self._reading = False
300 self._reading_callback = None
300 self._reading_callback = None
301 self._tab_width = 8
301 self._tab_width = 8
302
302
303 # List of strings pending to be appended as plain text in the widget.
303 # List of strings pending to be appended as plain text in the widget.
304 # The text is not immediately inserted when available to not
304 # The text is not immediately inserted when available to not
305 # choke the Qt event loop with paint events for the widget in
305 # choke the Qt event loop with paint events for the widget in
306 # case of lots of output from kernel.
306 # case of lots of output from kernel.
307 self._pending_insert_text = []
307 self._pending_insert_text = []
308
308
309 # Timer to flush the pending stream messages. The interval is adjusted
309 # Timer to flush the pending stream messages. The interval is adjusted
310 # later based on actual time taken for flushing a screen (buffer_size)
310 # later based on actual time taken for flushing a screen (buffer_size)
311 # of output text.
311 # of output text.
312 self._pending_text_flush_interval = QtCore.QTimer(self._control)
312 self._pending_text_flush_interval = QtCore.QTimer(self._control)
313 self._pending_text_flush_interval.setInterval(100)
313 self._pending_text_flush_interval.setInterval(100)
314 self._pending_text_flush_interval.setSingleShot(True)
314 self._pending_text_flush_interval.setSingleShot(True)
315 self._pending_text_flush_interval.timeout.connect(
315 self._pending_text_flush_interval.timeout.connect(
316 self._flush_pending_stream)
316 self._flush_pending_stream)
317
317
318 # Set a monospaced font.
318 # Set a monospaced font.
319 self.reset_font()
319 self.reset_font()
320
320
321 # Configure actions.
321 # Configure actions.
322 action = QtGui.QAction('Print', None)
322 action = QtGui.QAction('Print', None)
323 action.setEnabled(True)
323 action.setEnabled(True)
324 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
324 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
325 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
325 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
326 # Only override the default if there is a collision.
326 # Only override the default if there is a collision.
327 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
327 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
328 printkey = "Ctrl+Shift+P"
328 printkey = "Ctrl+Shift+P"
329 action.setShortcut(printkey)
329 action.setShortcut(printkey)
330 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
330 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
331 action.triggered.connect(self.print_)
331 action.triggered.connect(self.print_)
332 self.addAction(action)
332 self.addAction(action)
333 self.print_action = action
333 self.print_action = action
334
334
335 action = QtGui.QAction('Save as HTML/XML', None)
335 action = QtGui.QAction('Save as HTML/XML', None)
336 action.setShortcut(QtGui.QKeySequence.Save)
336 action.setShortcut(QtGui.QKeySequence.Save)
337 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
337 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
338 action.triggered.connect(self.export_html)
338 action.triggered.connect(self.export_html)
339 self.addAction(action)
339 self.addAction(action)
340 self.export_action = action
340 self.export_action = action
341
341
342 action = QtGui.QAction('Select All', None)
342 action = QtGui.QAction('Select All', None)
343 action.setEnabled(True)
343 action.setEnabled(True)
344 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
344 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
345 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
345 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
346 # Only override the default if there is a collision.
346 # Only override the default if there is a collision.
347 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
347 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
348 selectall = "Ctrl+Shift+A"
348 selectall = "Ctrl+Shift+A"
349 action.setShortcut(selectall)
349 action.setShortcut(selectall)
350 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
350 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
351 action.triggered.connect(self.select_all)
351 action.triggered.connect(self.select_all)
352 self.addAction(action)
352 self.addAction(action)
353 self.select_all_action = action
353 self.select_all_action = action
354
354
355 self.increase_font_size = QtGui.QAction("Bigger Font",
355 self.increase_font_size = QtGui.QAction("Bigger Font",
356 self,
356 self,
357 shortcut=QtGui.QKeySequence.ZoomIn,
357 shortcut=QtGui.QKeySequence.ZoomIn,
358 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
358 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
359 statusTip="Increase the font size by one point",
359 statusTip="Increase the font size by one point",
360 triggered=self._increase_font_size)
360 triggered=self._increase_font_size)
361 self.addAction(self.increase_font_size)
361 self.addAction(self.increase_font_size)
362
362
363 self.decrease_font_size = QtGui.QAction("Smaller Font",
363 self.decrease_font_size = QtGui.QAction("Smaller Font",
364 self,
364 self,
365 shortcut=QtGui.QKeySequence.ZoomOut,
365 shortcut=QtGui.QKeySequence.ZoomOut,
366 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
366 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
367 statusTip="Decrease the font size by one point",
367 statusTip="Decrease the font size by one point",
368 triggered=self._decrease_font_size)
368 triggered=self._decrease_font_size)
369 self.addAction(self.decrease_font_size)
369 self.addAction(self.decrease_font_size)
370
370
371 self.reset_font_size = QtGui.QAction("Normal Font",
371 self.reset_font_size = QtGui.QAction("Normal Font",
372 self,
372 self,
373 shortcut="Ctrl+0",
373 shortcut="Ctrl+0",
374 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
374 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
375 statusTip="Restore the Normal font size",
375 statusTip="Restore the Normal font size",
376 triggered=self.reset_font)
376 triggered=self.reset_font)
377 self.addAction(self.reset_font_size)
377 self.addAction(self.reset_font_size)
378
378
379 # Accept drag and drop events here. Drops were already turned off
379 # Accept drag and drop events here. Drops were already turned off
380 # in self._control when that widget was created.
380 # in self._control when that widget was created.
381 self.setAcceptDrops(True)
381 self.setAcceptDrops(True)
382
382
383 #---------------------------------------------------------------------------
383 #---------------------------------------------------------------------------
384 # Drag and drop support
384 # Drag and drop support
385 #---------------------------------------------------------------------------
385 #---------------------------------------------------------------------------
386
386
387 def dragEnterEvent(self, e):
387 def dragEnterEvent(self, e):
388 if e.mimeData().hasUrls():
388 if e.mimeData().hasUrls():
389 # The link action should indicate to that the drop will insert
389 # The link action should indicate to that the drop will insert
390 # the file anme.
390 # the file anme.
391 e.setDropAction(QtCore.Qt.LinkAction)
391 e.setDropAction(QtCore.Qt.LinkAction)
392 e.accept()
392 e.accept()
393 elif e.mimeData().hasText():
393 elif e.mimeData().hasText():
394 # By changing the action to copy we don't need to worry about
394 # By changing the action to copy we don't need to worry about
395 # the user accidentally moving text around in the widget.
395 # the user accidentally moving text around in the widget.
396 e.setDropAction(QtCore.Qt.CopyAction)
396 e.setDropAction(QtCore.Qt.CopyAction)
397 e.accept()
397 e.accept()
398
398
399 def dragMoveEvent(self, e):
399 def dragMoveEvent(self, e):
400 if e.mimeData().hasUrls():
400 if e.mimeData().hasUrls():
401 pass
401 pass
402 elif e.mimeData().hasText():
402 elif e.mimeData().hasText():
403 cursor = self._control.cursorForPosition(e.pos())
403 cursor = self._control.cursorForPosition(e.pos())
404 if self._in_buffer(cursor.position()):
404 if self._in_buffer(cursor.position()):
405 e.setDropAction(QtCore.Qt.CopyAction)
405 e.setDropAction(QtCore.Qt.CopyAction)
406 self._control.setTextCursor(cursor)
406 self._control.setTextCursor(cursor)
407 else:
407 else:
408 e.setDropAction(QtCore.Qt.IgnoreAction)
408 e.setDropAction(QtCore.Qt.IgnoreAction)
409 e.accept()
409 e.accept()
410
410
411 def dropEvent(self, e):
411 def dropEvent(self, e):
412 if e.mimeData().hasUrls():
412 if e.mimeData().hasUrls():
413 self._keep_cursor_in_buffer()
413 self._keep_cursor_in_buffer()
414 cursor = self._control.textCursor()
414 cursor = self._control.textCursor()
415 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
415 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
416 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
416 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
417 for f in filenames)
417 for f in filenames)
418 self._insert_plain_text_into_buffer(cursor, text)
418 self._insert_plain_text_into_buffer(cursor, text)
419 elif e.mimeData().hasText():
419 elif e.mimeData().hasText():
420 cursor = self._control.cursorForPosition(e.pos())
420 cursor = self._control.cursorForPosition(e.pos())
421 if self._in_buffer(cursor.position()):
421 if self._in_buffer(cursor.position()):
422 text = e.mimeData().text()
422 text = e.mimeData().text()
423 self._insert_plain_text_into_buffer(cursor, text)
423 self._insert_plain_text_into_buffer(cursor, text)
424
424
425 def eventFilter(self, obj, event):
425 def eventFilter(self, obj, event):
426 """ Reimplemented to ensure a console-like behavior in the underlying
426 """ Reimplemented to ensure a console-like behavior in the underlying
427 text widgets.
427 text widgets.
428 """
428 """
429 etype = event.type()
429 etype = event.type()
430 if etype == QtCore.QEvent.KeyPress:
430 if etype == QtCore.QEvent.KeyPress:
431
431
432 # Re-map keys for all filtered widgets.
432 # Re-map keys for all filtered widgets.
433 key = event.key()
433 key = event.key()
434 if self._control_key_down(event.modifiers()) and \
434 if self._control_key_down(event.modifiers()) and \
435 key in self._ctrl_down_remap:
435 key in self._ctrl_down_remap:
436 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
436 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
437 self._ctrl_down_remap[key],
437 self._ctrl_down_remap[key],
438 QtCore.Qt.NoModifier)
438 QtCore.Qt.NoModifier)
439 QtGui.qApp.sendEvent(obj, new_event)
439 QtGui.qApp.sendEvent(obj, new_event)
440 return True
440 return True
441
441
442 elif obj == self._control:
442 elif obj == self._control:
443 return self._event_filter_console_keypress(event)
443 return self._event_filter_console_keypress(event)
444
444
445 elif obj == self._page_control:
445 elif obj == self._page_control:
446 return self._event_filter_page_keypress(event)
446 return self._event_filter_page_keypress(event)
447
447
448 # Make middle-click paste safe.
448 # Make middle-click paste safe.
449 elif etype == QtCore.QEvent.MouseButtonRelease and \
449 elif etype == QtCore.QEvent.MouseButtonRelease and \
450 event.button() == QtCore.Qt.MidButton and \
450 event.button() == QtCore.Qt.MidButton and \
451 obj == self._control.viewport():
451 obj == self._control.viewport():
452 cursor = self._control.cursorForPosition(event.pos())
452 cursor = self._control.cursorForPosition(event.pos())
453 self._control.setTextCursor(cursor)
453 self._control.setTextCursor(cursor)
454 self.paste(QtGui.QClipboard.Selection)
454 self.paste(QtGui.QClipboard.Selection)
455 return True
455 return True
456
456
457 # Manually adjust the scrollbars *after* a resize event is dispatched.
457 # Manually adjust the scrollbars *after* a resize event is dispatched.
458 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
458 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
459 self._filter_resize = True
459 self._filter_resize = True
460 QtGui.qApp.sendEvent(obj, event)
460 QtGui.qApp.sendEvent(obj, event)
461 self._adjust_scrollbars()
461 self._adjust_scrollbars()
462 self._filter_resize = False
462 self._filter_resize = False
463 return True
463 return True
464
464
465 # Override shortcuts for all filtered widgets.
465 # Override shortcuts for all filtered widgets.
466 elif etype == QtCore.QEvent.ShortcutOverride and \
466 elif etype == QtCore.QEvent.ShortcutOverride and \
467 self.override_shortcuts and \
467 self.override_shortcuts and \
468 self._control_key_down(event.modifiers()) and \
468 self._control_key_down(event.modifiers()) and \
469 event.key() in self._shortcuts:
469 event.key() in self._shortcuts:
470 event.accept()
470 event.accept()
471
471
472 # Handle scrolling of the vsplit pager. This hack attempts to solve
472 # Handle scrolling of the vsplit pager. This hack attempts to solve
473 # problems with tearing of the help text inside the pager window. This
473 # problems with tearing of the help text inside the pager window. This
474 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
474 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
475 # perfect but makes the pager more usable.
475 # perfect but makes the pager more usable.
476 elif etype in self._pager_scroll_events and \
476 elif etype in self._pager_scroll_events and \
477 obj == self._page_control:
477 obj == self._page_control:
478 self._page_control.repaint()
478 self._page_control.repaint()
479 return True
479 return True
480
480
481 elif etype == QtCore.QEvent.MouseMove:
481 elif etype == QtCore.QEvent.MouseMove:
482 anchor = self._control.anchorAt(event.pos())
482 anchor = self._control.anchorAt(event.pos())
483 QtGui.QToolTip.showText(event.globalPos(), anchor)
483 QtGui.QToolTip.showText(event.globalPos(), anchor)
484
484
485 return super(ConsoleWidget, self).eventFilter(obj, event)
485 return super(ConsoleWidget, self).eventFilter(obj, event)
486
486
487 #---------------------------------------------------------------------------
487 #---------------------------------------------------------------------------
488 # 'QWidget' interface
488 # 'QWidget' interface
489 #---------------------------------------------------------------------------
489 #---------------------------------------------------------------------------
490
490
491 def sizeHint(self):
491 def sizeHint(self):
492 """ Reimplemented to suggest a size that is 80 characters wide and
492 """ Reimplemented to suggest a size that is 80 characters wide and
493 25 lines high.
493 25 lines high.
494 """
494 """
495 font_metrics = QtGui.QFontMetrics(self.font)
495 font_metrics = QtGui.QFontMetrics(self.font)
496 margin = (self._control.frameWidth() +
496 margin = (self._control.frameWidth() +
497 self._control.document().documentMargin()) * 2
497 self._control.document().documentMargin()) * 2
498 style = self.style()
498 style = self.style()
499 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
499 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
500
500
501 # Note 1: Despite my best efforts to take the various margins into
501 # Note 1: Despite my best efforts to take the various margins into
502 # account, the width is still coming out a bit too small, so we include
502 # account, the width is still coming out a bit too small, so we include
503 # a fudge factor of one character here.
503 # a fudge factor of one character here.
504 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
504 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
505 # to a Qt bug on certain Mac OS systems where it returns 0.
505 # to a Qt bug on certain Mac OS systems where it returns 0.
506 width = font_metrics.width(' ') * self.width + margin
506 width = font_metrics.width(' ') * self.width + margin
507 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
507 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
508 if self.paging == 'hsplit':
508 if self.paging == 'hsplit':
509 width = width * 2 + splitwidth
509 width = width * 2 + splitwidth
510
510
511 height = font_metrics.height() * self.height + margin
511 height = font_metrics.height() * self.height + margin
512 if self.paging == 'vsplit':
512 if self.paging == 'vsplit':
513 height = height * 2 + splitwidth
513 height = height * 2 + splitwidth
514
514
515 return QtCore.QSize(width, height)
515 return QtCore.QSize(width, height)
516
516
517 #---------------------------------------------------------------------------
517 #---------------------------------------------------------------------------
518 # 'ConsoleWidget' public interface
518 # 'ConsoleWidget' public interface
519 #---------------------------------------------------------------------------
519 #---------------------------------------------------------------------------
520
520
521 def can_copy(self):
521 def can_copy(self):
522 """ Returns whether text can be copied to the clipboard.
522 """ Returns whether text can be copied to the clipboard.
523 """
523 """
524 return self._control.textCursor().hasSelection()
524 return self._control.textCursor().hasSelection()
525
525
526 def can_cut(self):
526 def can_cut(self):
527 """ Returns whether text can be cut to the clipboard.
527 """ Returns whether text can be cut to the clipboard.
528 """
528 """
529 cursor = self._control.textCursor()
529 cursor = self._control.textCursor()
530 return (cursor.hasSelection() and
530 return (cursor.hasSelection() and
531 self._in_buffer(cursor.anchor()) and
531 self._in_buffer(cursor.anchor()) and
532 self._in_buffer(cursor.position()))
532 self._in_buffer(cursor.position()))
533
533
534 def can_paste(self):
534 def can_paste(self):
535 """ Returns whether text can be pasted from the clipboard.
535 """ Returns whether text can be pasted from the clipboard.
536 """
536 """
537 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
537 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
538 return bool(QtGui.QApplication.clipboard().text())
538 return bool(QtGui.QApplication.clipboard().text())
539 return False
539 return False
540
540
541 def clear(self, keep_input=True):
541 def clear(self, keep_input=True):
542 """ Clear the console.
542 """ Clear the console.
543
543
544 Parameters:
544 Parameters:
545 -----------
545 -----------
546 keep_input : bool, optional (default True)
546 keep_input : bool, optional (default True)
547 If set, restores the old input buffer if a new prompt is written.
547 If set, restores the old input buffer if a new prompt is written.
548 """
548 """
549 if self._executing:
549 if self._executing:
550 self._control.clear()
550 self._control.clear()
551 else:
551 else:
552 if keep_input:
552 if keep_input:
553 input_buffer = self.input_buffer
553 input_buffer = self.input_buffer
554 self._control.clear()
554 self._control.clear()
555 self._show_prompt()
555 self._show_prompt()
556 if keep_input:
556 if keep_input:
557 self.input_buffer = input_buffer
557 self.input_buffer = input_buffer
558
558
559 def copy(self):
559 def copy(self):
560 """ Copy the currently selected text to the clipboard.
560 """ Copy the currently selected text to the clipboard.
561 """
561 """
562 self.layout().currentWidget().copy()
562 self.layout().currentWidget().copy()
563
563
564 def copy_anchor(self, anchor):
564 def copy_anchor(self, anchor):
565 """ Copy anchor text to the clipboard
565 """ Copy anchor text to the clipboard
566 """
566 """
567 QtGui.QApplication.clipboard().setText(anchor)
567 QtGui.QApplication.clipboard().setText(anchor)
568
568
569 def cut(self):
569 def cut(self):
570 """ Copy the currently selected text to the clipboard and delete it
570 """ Copy the currently selected text to the clipboard and delete it
571 if it's inside the input buffer.
571 if it's inside the input buffer.
572 """
572 """
573 self.copy()
573 self.copy()
574 if self.can_cut():
574 if self.can_cut():
575 self._control.textCursor().removeSelectedText()
575 self._control.textCursor().removeSelectedText()
576
576
577 def execute(self, source=None, hidden=False, interactive=False):
577 def execute(self, source=None, hidden=False, interactive=False):
578 """ Executes source or the input buffer, possibly prompting for more
578 """ Executes source or the input buffer, possibly prompting for more
579 input.
579 input.
580
580
581 Parameters:
581 Parameters:
582 -----------
582 -----------
583 source : str, optional
583 source : str, optional
584
584
585 The source to execute. If not specified, the input buffer will be
585 The source to execute. If not specified, the input buffer will be
586 used. If specified and 'hidden' is False, the input buffer will be
586 used. If specified and 'hidden' is False, the input buffer will be
587 replaced with the source before execution.
587 replaced with the source before execution.
588
588
589 hidden : bool, optional (default False)
589 hidden : bool, optional (default False)
590
590
591 If set, no output will be shown and the prompt will not be modified.
591 If set, no output will be shown and the prompt will not be modified.
592 In other words, it will be completely invisible to the user that
592 In other words, it will be completely invisible to the user that
593 an execution has occurred.
593 an execution has occurred.
594
594
595 interactive : bool, optional (default False)
595 interactive : bool, optional (default False)
596
596
597 Whether the console is to treat the source as having been manually
597 Whether the console is to treat the source as having been manually
598 entered by the user. The effect of this parameter depends on the
598 entered by the user. The effect of this parameter depends on the
599 subclass implementation.
599 subclass implementation.
600
600
601 Raises:
601 Raises:
602 -------
602 -------
603 RuntimeError
603 RuntimeError
604 If incomplete input is given and 'hidden' is True. In this case,
604 If incomplete input is given and 'hidden' is True. In this case,
605 it is not possible to prompt for more input.
605 it is not possible to prompt for more input.
606
606
607 Returns:
607 Returns:
608 --------
608 --------
609 A boolean indicating whether the source was executed.
609 A boolean indicating whether the source was executed.
610 """
610 """
611 # WARNING: The order in which things happen here is very particular, in
611 # WARNING: The order in which things happen here is very particular, in
612 # large part because our syntax highlighting is fragile. If you change
612 # large part because our syntax highlighting is fragile. If you change
613 # something, test carefully!
613 # something, test carefully!
614
614
615 # Decide what to execute.
615 # Decide what to execute.
616 if source is None:
616 if source is None:
617 source = self.input_buffer
617 source = self.input_buffer
618 if not hidden:
618 if not hidden:
619 # A newline is appended later, but it should be considered part
619 # A newline is appended later, but it should be considered part
620 # of the input buffer.
620 # of the input buffer.
621 source += '\n'
621 source += '\n'
622 elif not hidden:
622 elif not hidden:
623 self.input_buffer = source
623 self.input_buffer = source
624
624
625 # Execute the source or show a continuation prompt if it is incomplete.
625 # Execute the source or show a continuation prompt if it is incomplete.
626 if self.execute_on_complete_input:
626 if self.execute_on_complete_input:
627 complete = self._is_complete(source, interactive)
627 complete = self._is_complete(source, interactive)
628 else:
628 else:
629 complete = not interactive
629 complete = not interactive
630 if hidden:
630 if hidden:
631 if complete or not self.execute_on_complete_input:
631 if complete or not self.execute_on_complete_input:
632 self._execute(source, hidden)
632 self._execute(source, hidden)
633 else:
633 else:
634 error = 'Incomplete noninteractive input: "%s"'
634 error = 'Incomplete noninteractive input: "%s"'
635 raise RuntimeError(error % source)
635 raise RuntimeError(error % source)
636 else:
636 else:
637 if complete:
637 if complete:
638 self._append_plain_text('\n')
638 self._append_plain_text('\n')
639 self._input_buffer_executing = self.input_buffer
639 self._input_buffer_executing = self.input_buffer
640 self._executing = True
640 self._executing = True
641 self._prompt_finished()
641 self._prompt_finished()
642
642
643 # The maximum block count is only in effect during execution.
643 # The maximum block count is only in effect during execution.
644 # This ensures that _prompt_pos does not become invalid due to
644 # This ensures that _prompt_pos does not become invalid due to
645 # text truncation.
645 # text truncation.
646 self._control.document().setMaximumBlockCount(self.buffer_size)
646 self._control.document().setMaximumBlockCount(self.buffer_size)
647
647
648 # Setting a positive maximum block count will automatically
648 # Setting a positive maximum block count will automatically
649 # disable the undo/redo history, but just to be safe:
649 # disable the undo/redo history, but just to be safe:
650 self._control.setUndoRedoEnabled(False)
650 self._control.setUndoRedoEnabled(False)
651
651
652 # Perform actual execution.
652 # Perform actual execution.
653 self._execute(source, hidden)
653 self._execute(source, hidden)
654
654
655 else:
655 else:
656 # Do this inside an edit block so continuation prompts are
656 # Do this inside an edit block so continuation prompts are
657 # removed seamlessly via undo/redo.
657 # removed seamlessly via undo/redo.
658 cursor = self._get_end_cursor()
658 cursor = self._get_end_cursor()
659 cursor.beginEditBlock()
659 cursor.beginEditBlock()
660 cursor.insertText('\n')
660 cursor.insertText('\n')
661 self._insert_continuation_prompt(cursor)
661 self._insert_continuation_prompt(cursor)
662 cursor.endEditBlock()
662 cursor.endEditBlock()
663
663
664 # Do not do this inside the edit block. It works as expected
664 # Do not do this inside the edit block. It works as expected
665 # when using a QPlainTextEdit control, but does not have an
665 # when using a QPlainTextEdit control, but does not have an
666 # effect when using a QTextEdit. I believe this is a Qt bug.
666 # effect when using a QTextEdit. I believe this is a Qt bug.
667 self._control.moveCursor(QtGui.QTextCursor.End)
667 self._control.moveCursor(QtGui.QTextCursor.End)
668
668
669 return complete
669 return complete
670
670
671 def export_html(self):
671 def export_html(self):
672 """ Shows a dialog to export HTML/XML in various formats.
672 """ Shows a dialog to export HTML/XML in various formats.
673 """
673 """
674 self._html_exporter.export()
674 self._html_exporter.export()
675
675
676 def _get_input_buffer(self, force=False):
676 def _get_input_buffer(self, force=False):
677 """ The text that the user has entered entered at the current prompt.
677 """ The text that the user has entered entered at the current prompt.
678
678
679 If the console is currently executing, the text that is executing will
679 If the console is currently executing, the text that is executing will
680 always be returned.
680 always be returned.
681 """
681 """
682 # If we're executing, the input buffer may not even exist anymore due to
682 # If we're executing, the input buffer may not even exist anymore due to
683 # the limit imposed by 'buffer_size'. Therefore, we store it.
683 # the limit imposed by 'buffer_size'. Therefore, we store it.
684 if self._executing and not force:
684 if self._executing and not force:
685 return self._input_buffer_executing
685 return self._input_buffer_executing
686
686
687 cursor = self._get_end_cursor()
687 cursor = self._get_end_cursor()
688 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
688 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
689 input_buffer = cursor.selection().toPlainText()
689 input_buffer = cursor.selection().toPlainText()
690
690
691 # Strip out continuation prompts.
691 # Strip out continuation prompts.
692 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
692 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
693
693
694 def _set_input_buffer(self, string):
694 def _set_input_buffer(self, string):
695 """ Sets the text in the input buffer.
695 """ Sets the text in the input buffer.
696
696
697 If the console is currently executing, this call has no *immediate*
697 If the console is currently executing, this call has no *immediate*
698 effect. When the execution is finished, the input buffer will be updated
698 effect. When the execution is finished, the input buffer will be updated
699 appropriately.
699 appropriately.
700 """
700 """
701 # If we're executing, store the text for later.
701 # If we're executing, store the text for later.
702 if self._executing:
702 if self._executing:
703 self._input_buffer_pending = string
703 self._input_buffer_pending = string
704 return
704 return
705
705
706 # Remove old text.
706 # Remove old text.
707 cursor = self._get_end_cursor()
707 cursor = self._get_end_cursor()
708 cursor.beginEditBlock()
708 cursor.beginEditBlock()
709 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
709 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
710 cursor.removeSelectedText()
710 cursor.removeSelectedText()
711
711
712 # Insert new text with continuation prompts.
712 # Insert new text with continuation prompts.
713 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
713 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
714 cursor.endEditBlock()
714 cursor.endEditBlock()
715 self._control.moveCursor(QtGui.QTextCursor.End)
715 self._control.moveCursor(QtGui.QTextCursor.End)
716
716
717 input_buffer = property(_get_input_buffer, _set_input_buffer)
717 input_buffer = property(_get_input_buffer, _set_input_buffer)
718
718
719 def _get_font(self):
719 def _get_font(self):
720 """ The base font being used by the ConsoleWidget.
720 """ The base font being used by the ConsoleWidget.
721 """
721 """
722 return self._control.document().defaultFont()
722 return self._control.document().defaultFont()
723
723
724 def _set_font(self, font):
724 def _set_font(self, font):
725 """ Sets the base font for the ConsoleWidget to the specified QFont.
725 """ Sets the base font for the ConsoleWidget to the specified QFont.
726 """
726 """
727 font_metrics = QtGui.QFontMetrics(font)
727 font_metrics = QtGui.QFontMetrics(font)
728 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
728 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
729
729
730 self._completion_widget.setFont(font)
730 self._completion_widget.setFont(font)
731 self._control.document().setDefaultFont(font)
731 self._control.document().setDefaultFont(font)
732 if self._page_control:
732 if self._page_control:
733 self._page_control.document().setDefaultFont(font)
733 self._page_control.document().setDefaultFont(font)
734
734
735 self.font_changed.emit(font)
735 self.font_changed.emit(font)
736
736
737 font = property(_get_font, _set_font)
737 font = property(_get_font, _set_font)
738
738
739 def open_anchor(self, anchor):
739 def open_anchor(self, anchor):
740 """ Open selected anchor in the default webbrowser
740 """ Open selected anchor in the default webbrowser
741 """
741 """
742 webbrowser.open( anchor )
742 webbrowser.open( anchor )
743
743
744 def paste(self, mode=QtGui.QClipboard.Clipboard):
744 def paste(self, mode=QtGui.QClipboard.Clipboard):
745 """ Paste the contents of the clipboard into the input region.
745 """ Paste the contents of the clipboard into the input region.
746
746
747 Parameters:
747 Parameters:
748 -----------
748 -----------
749 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
749 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
750
750
751 Controls which part of the system clipboard is used. This can be
751 Controls which part of the system clipboard is used. This can be
752 used to access the selection clipboard in X11 and the Find buffer
752 used to access the selection clipboard in X11 and the Find buffer
753 in Mac OS. By default, the regular clipboard is used.
753 in Mac OS. By default, the regular clipboard is used.
754 """
754 """
755 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
755 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
756 # Make sure the paste is safe.
756 # Make sure the paste is safe.
757 self._keep_cursor_in_buffer()
757 self._keep_cursor_in_buffer()
758 cursor = self._control.textCursor()
758 cursor = self._control.textCursor()
759
759
760 # Remove any trailing newline, which confuses the GUI and forces the
760 # Remove any trailing newline, which confuses the GUI and forces the
761 # user to backspace.
761 # user to backspace.
762 text = QtGui.QApplication.clipboard().text(mode).rstrip()
762 text = QtGui.QApplication.clipboard().text(mode).rstrip()
763 self._insert_plain_text_into_buffer(cursor, dedent(text))
763 self._insert_plain_text_into_buffer(cursor, dedent(text))
764
764
765 def print_(self, printer = None):
765 def print_(self, printer = None):
766 """ Print the contents of the ConsoleWidget to the specified QPrinter.
766 """ Print the contents of the ConsoleWidget to the specified QPrinter.
767 """
767 """
768 if (not printer):
768 if (not printer):
769 printer = QtGui.QPrinter()
769 printer = QtGui.QPrinter()
770 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
770 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
771 return
771 return
772 self._control.print_(printer)
772 self._control.print_(printer)
773
773
774 def prompt_to_top(self):
774 def prompt_to_top(self):
775 """ Moves the prompt to the top of the viewport.
775 """ Moves the prompt to the top of the viewport.
776 """
776 """
777 if not self._executing:
777 if not self._executing:
778 prompt_cursor = self._get_prompt_cursor()
778 prompt_cursor = self._get_prompt_cursor()
779 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
779 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
780 self._set_cursor(prompt_cursor)
780 self._set_cursor(prompt_cursor)
781 self._set_top_cursor(prompt_cursor)
781 self._set_top_cursor(prompt_cursor)
782
782
783 def redo(self):
783 def redo(self):
784 """ Redo the last operation. If there is no operation to redo, nothing
784 """ Redo the last operation. If there is no operation to redo, nothing
785 happens.
785 happens.
786 """
786 """
787 self._control.redo()
787 self._control.redo()
788
788
789 def reset_font(self):
789 def reset_font(self):
790 """ Sets the font to the default fixed-width font for this platform.
790 """ Sets the font to the default fixed-width font for this platform.
791 """
791 """
792 if sys.platform == 'win32':
792 if sys.platform == 'win32':
793 # Consolas ships with Vista/Win7, fallback to Courier if needed
793 # Consolas ships with Vista/Win7, fallback to Courier if needed
794 fallback = 'Courier'
794 fallback = 'Courier'
795 elif sys.platform == 'darwin':
795 elif sys.platform == 'darwin':
796 # OSX always has Monaco
796 # OSX always has Monaco
797 fallback = 'Monaco'
797 fallback = 'Monaco'
798 else:
798 else:
799 # Monospace should always exist
799 # Monospace should always exist
800 fallback = 'Monospace'
800 fallback = 'Monospace'
801 font = get_font(self.font_family, fallback)
801 font = get_font(self.font_family, fallback)
802 if self.font_size:
802 if self.font_size:
803 font.setPointSize(self.font_size)
803 font.setPointSize(self.font_size)
804 else:
804 else:
805 font.setPointSize(QtGui.qApp.font().pointSize())
805 font.setPointSize(QtGui.qApp.font().pointSize())
806 font.setStyleHint(QtGui.QFont.TypeWriter)
806 font.setStyleHint(QtGui.QFont.TypeWriter)
807 self._set_font(font)
807 self._set_font(font)
808
808
809 def change_font_size(self, delta):
809 def change_font_size(self, delta):
810 """Change the font size by the specified amount (in points).
810 """Change the font size by the specified amount (in points).
811 """
811 """
812 font = self.font
812 font = self.font
813 size = max(font.pointSize() + delta, 1) # minimum 1 point
813 size = max(font.pointSize() + delta, 1) # minimum 1 point
814 font.setPointSize(size)
814 font.setPointSize(size)
815 self._set_font(font)
815 self._set_font(font)
816
816
817 def _increase_font_size(self):
817 def _increase_font_size(self):
818 self.change_font_size(1)
818 self.change_font_size(1)
819
819
820 def _decrease_font_size(self):
820 def _decrease_font_size(self):
821 self.change_font_size(-1)
821 self.change_font_size(-1)
822
822
823 def select_all(self):
823 def select_all(self):
824 """ Selects all the text in the buffer.
824 """ Selects all the text in the buffer.
825 """
825 """
826 self._control.selectAll()
826 self._control.selectAll()
827
827
828 def _get_tab_width(self):
828 def _get_tab_width(self):
829 """ The width (in terms of space characters) for tab characters.
829 """ The width (in terms of space characters) for tab characters.
830 """
830 """
831 return self._tab_width
831 return self._tab_width
832
832
833 def _set_tab_width(self, tab_width):
833 def _set_tab_width(self, tab_width):
834 """ Sets the width (in terms of space characters) for tab characters.
834 """ Sets the width (in terms of space characters) for tab characters.
835 """
835 """
836 font_metrics = QtGui.QFontMetrics(self.font)
836 font_metrics = QtGui.QFontMetrics(self.font)
837 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
837 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
838
838
839 self._tab_width = tab_width
839 self._tab_width = tab_width
840
840
841 tab_width = property(_get_tab_width, _set_tab_width)
841 tab_width = property(_get_tab_width, _set_tab_width)
842
842
843 def undo(self):
843 def undo(self):
844 """ Undo the last operation. If there is no operation to undo, nothing
844 """ Undo the last operation. If there is no operation to undo, nothing
845 happens.
845 happens.
846 """
846 """
847 self._control.undo()
847 self._control.undo()
848
848
849 #---------------------------------------------------------------------------
849 #---------------------------------------------------------------------------
850 # 'ConsoleWidget' abstract interface
850 # 'ConsoleWidget' abstract interface
851 #---------------------------------------------------------------------------
851 #---------------------------------------------------------------------------
852
852
853 def _is_complete(self, source, interactive):
853 def _is_complete(self, source, interactive):
854 """ Returns whether 'source' can be executed. When triggered by an
854 """ Returns whether 'source' can be executed. When triggered by an
855 Enter/Return key press, 'interactive' is True; otherwise, it is
855 Enter/Return key press, 'interactive' is True; otherwise, it is
856 False.
856 False.
857 """
857 """
858 raise NotImplementedError
858 raise NotImplementedError
859
859
860 def _execute(self, source, hidden):
860 def _execute(self, source, hidden):
861 """ Execute 'source'. If 'hidden', do not show any output.
861 """ Execute 'source'. If 'hidden', do not show any output.
862 """
862 """
863 raise NotImplementedError
863 raise NotImplementedError
864
864
865 def _prompt_started_hook(self):
865 def _prompt_started_hook(self):
866 """ Called immediately after a new prompt is displayed.
866 """ Called immediately after a new prompt is displayed.
867 """
867 """
868 pass
868 pass
869
869
870 def _prompt_finished_hook(self):
870 def _prompt_finished_hook(self):
871 """ Called immediately after a prompt is finished, i.e. when some input
871 """ Called immediately after a prompt is finished, i.e. when some input
872 will be processed and a new prompt displayed.
872 will be processed and a new prompt displayed.
873 """
873 """
874 pass
874 pass
875
875
876 def _up_pressed(self, shift_modifier):
876 def _up_pressed(self, shift_modifier):
877 """ Called when the up key is pressed. Returns whether to continue
877 """ Called when the up key is pressed. Returns whether to continue
878 processing the event.
878 processing the event.
879 """
879 """
880 return True
880 return True
881
881
882 def _down_pressed(self, shift_modifier):
882 def _down_pressed(self, shift_modifier):
883 """ Called when the down key is pressed. Returns whether to continue
883 """ Called when the down key is pressed. Returns whether to continue
884 processing the event.
884 processing the event.
885 """
885 """
886 return True
886 return True
887
887
888 def _tab_pressed(self):
888 def _tab_pressed(self):
889 """ Called when the tab key is pressed. Returns whether to continue
889 """ Called when the tab key is pressed. Returns whether to continue
890 processing the event.
890 processing the event.
891 """
891 """
892 return False
892 return False
893
893
894 #--------------------------------------------------------------------------
894 #--------------------------------------------------------------------------
895 # 'ConsoleWidget' protected interface
895 # 'ConsoleWidget' protected interface
896 #--------------------------------------------------------------------------
896 #--------------------------------------------------------------------------
897
897
898 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
898 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
899 """ A low-level method for appending content to the end of the buffer.
899 """ A low-level method for appending content to the end of the buffer.
900
900
901 If 'before_prompt' is enabled, the content will be inserted before the
901 If 'before_prompt' is enabled, the content will be inserted before the
902 current prompt, if there is one.
902 current prompt, if there is one.
903 """
903 """
904 # Determine where to insert the content.
904 # Determine where to insert the content.
905 cursor = self._control.textCursor()
905 cursor = self._control.textCursor()
906 if before_prompt and (self._reading or not self._executing):
906 if before_prompt and (self._reading or not self._executing):
907 self._flush_pending_stream()
907 self._flush_pending_stream()
908 cursor.setPosition(self._append_before_prompt_pos)
908 cursor.setPosition(self._append_before_prompt_pos)
909 else:
909 else:
910 if insert != self._insert_plain_text:
910 if insert != self._insert_plain_text:
911 self._flush_pending_stream()
911 self._flush_pending_stream()
912 cursor.movePosition(QtGui.QTextCursor.End)
912 cursor.movePosition(QtGui.QTextCursor.End)
913 start_pos = cursor.position()
913 start_pos = cursor.position()
914
914
915 # Perform the insertion.
915 # Perform the insertion.
916 result = insert(cursor, input, *args, **kwargs)
916 result = insert(cursor, input, *args, **kwargs)
917
917
918 # Adjust the prompt position if we have inserted before it. This is safe
918 # Adjust the prompt position if we have inserted before it. This is safe
919 # because buffer truncation is disabled when not executing.
919 # because buffer truncation is disabled when not executing.
920 if before_prompt and not self._executing:
920 if before_prompt and not self._executing:
921 diff = cursor.position() - start_pos
921 diff = cursor.position() - start_pos
922 self._append_before_prompt_pos += diff
922 self._append_before_prompt_pos += diff
923 self._prompt_pos += diff
923 self._prompt_pos += diff
924
924
925 return result
925 return result
926
926
927 def _append_block(self, block_format=None, before_prompt=False):
927 def _append_block(self, block_format=None, before_prompt=False):
928 """ Appends an new QTextBlock to the end of the console buffer.
928 """ Appends an new QTextBlock to the end of the console buffer.
929 """
929 """
930 self._append_custom(self._insert_block, block_format, before_prompt)
930 self._append_custom(self._insert_block, block_format, before_prompt)
931
931
932 def _append_html(self, html, before_prompt=False):
932 def _append_html(self, html, before_prompt=False):
933 """ Appends HTML at the end of the console buffer.
933 """ Appends HTML at the end of the console buffer.
934 """
934 """
935 self._append_custom(self._insert_html, html, before_prompt)
935 self._append_custom(self._insert_html, html, before_prompt)
936
936
937 def _append_html_fetching_plain_text(self, html, before_prompt=False):
937 def _append_html_fetching_plain_text(self, html, before_prompt=False):
938 """ Appends HTML, then returns the plain text version of it.
938 """ Appends HTML, then returns the plain text version of it.
939 """
939 """
940 return self._append_custom(self._insert_html_fetching_plain_text,
940 return self._append_custom(self._insert_html_fetching_plain_text,
941 html, before_prompt)
941 html, before_prompt)
942
942
943 def _append_plain_text(self, text, before_prompt=False):
943 def _append_plain_text(self, text, before_prompt=False):
944 """ Appends plain text, processing ANSI codes if enabled.
944 """ Appends plain text, processing ANSI codes if enabled.
945 """
945 """
946 self._append_custom(self._insert_plain_text, text, before_prompt)
946 self._append_custom(self._insert_plain_text, text, before_prompt)
947
947
948 def _cancel_completion(self):
948 def _cancel_completion(self):
949 """ If text completion is progress, cancel it.
949 """ If text completion is progress, cancel it.
950 """
950 """
951 self._completion_widget.cancel_completion()
951 self._completion_widget.cancel_completion()
952
952
953 def _clear_temporary_buffer(self):
953 def _clear_temporary_buffer(self):
954 """ Clears the "temporary text" buffer, i.e. all the text following
954 """ Clears the "temporary text" buffer, i.e. all the text following
955 the prompt region.
955 the prompt region.
956 """
956 """
957 # Select and remove all text below the input buffer.
957 # Select and remove all text below the input buffer.
958 cursor = self._get_prompt_cursor()
958 cursor = self._get_prompt_cursor()
959 prompt = self._continuation_prompt.lstrip()
959 prompt = self._continuation_prompt.lstrip()
960 if(self._temp_buffer_filled):
960 if(self._temp_buffer_filled):
961 self._temp_buffer_filled = False
961 self._temp_buffer_filled = False
962 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
962 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
963 temp_cursor = QtGui.QTextCursor(cursor)
963 temp_cursor = QtGui.QTextCursor(cursor)
964 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
964 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
965 text = temp_cursor.selection().toPlainText().lstrip()
965 text = temp_cursor.selection().toPlainText().lstrip()
966 if not text.startswith(prompt):
966 if not text.startswith(prompt):
967 break
967 break
968 else:
968 else:
969 # We've reached the end of the input buffer and no text follows.
969 # We've reached the end of the input buffer and no text follows.
970 return
970 return
971 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
971 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
972 cursor.movePosition(QtGui.QTextCursor.End,
972 cursor.movePosition(QtGui.QTextCursor.End,
973 QtGui.QTextCursor.KeepAnchor)
973 QtGui.QTextCursor.KeepAnchor)
974 cursor.removeSelectedText()
974 cursor.removeSelectedText()
975
975
976 # After doing this, we have no choice but to clear the undo/redo
976 # After doing this, we have no choice but to clear the undo/redo
977 # history. Otherwise, the text is not "temporary" at all, because it
977 # history. Otherwise, the text is not "temporary" at all, because it
978 # can be recalled with undo/redo. Unfortunately, Qt does not expose
978 # can be recalled with undo/redo. Unfortunately, Qt does not expose
979 # fine-grained control to the undo/redo system.
979 # fine-grained control to the undo/redo system.
980 if self._control.isUndoRedoEnabled():
980 if self._control.isUndoRedoEnabled():
981 self._control.setUndoRedoEnabled(False)
981 self._control.setUndoRedoEnabled(False)
982 self._control.setUndoRedoEnabled(True)
982 self._control.setUndoRedoEnabled(True)
983
983
984 def _complete_with_items(self, cursor, items):
984 def _complete_with_items(self, cursor, items):
985 """ Performs completion with 'items' at the specified cursor location.
985 """ Performs completion with 'items' at the specified cursor location.
986 """
986 """
987 self._cancel_completion()
987 self._cancel_completion()
988
988
989 if len(items) == 1:
989 if len(items) == 1:
990 cursor.setPosition(self._control.textCursor().position(),
990 cursor.setPosition(self._control.textCursor().position(),
991 QtGui.QTextCursor.KeepAnchor)
991 QtGui.QTextCursor.KeepAnchor)
992 cursor.insertText(items[0])
992 cursor.insertText(items[0])
993
993
994 elif len(items) > 1:
994 elif len(items) > 1:
995 current_pos = self._control.textCursor().position()
995 current_pos = self._control.textCursor().position()
996 prefix = commonprefix(items)
996 prefix = commonprefix(items)
997 if prefix:
997 if prefix:
998 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
998 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
999 cursor.insertText(prefix)
999 cursor.insertText(prefix)
1000 current_pos = cursor.position()
1000 current_pos = cursor.position()
1001
1001
1002 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1002 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1003 self._completion_widget.show_items(cursor, items)
1003 self._completion_widget.show_items(cursor, items)
1004
1004
1005
1005
1006 def _fill_temporary_buffer(self, cursor, text, html=False):
1006 def _fill_temporary_buffer(self, cursor, text, html=False):
1007 """fill the area below the active editting zone with text"""
1007 """fill the area below the active editting zone with text"""
1008
1008
1009 current_pos = self._control.textCursor().position()
1009 current_pos = self._control.textCursor().position()
1010
1010
1011 cursor.beginEditBlock()
1011 cursor.beginEditBlock()
1012 self._append_plain_text('\n')
1012 self._append_plain_text('\n')
1013 self._page(text, html=html)
1013 self._page(text, html=html)
1014 cursor.endEditBlock()
1014 cursor.endEditBlock()
1015
1015
1016 cursor.setPosition(current_pos)
1016 cursor.setPosition(current_pos)
1017 self._control.moveCursor(QtGui.QTextCursor.End)
1017 self._control.moveCursor(QtGui.QTextCursor.End)
1018 self._control.setTextCursor(cursor)
1018 self._control.setTextCursor(cursor)
1019
1019
1020 self._temp_buffer_filled = True
1020 self._temp_buffer_filled = True
1021
1021
1022
1022
1023 def _context_menu_make(self, pos):
1023 def _context_menu_make(self, pos):
1024 """ Creates a context menu for the given QPoint (in widget coordinates).
1024 """ Creates a context menu for the given QPoint (in widget coordinates).
1025 """
1025 """
1026 menu = QtGui.QMenu(self)
1026 menu = QtGui.QMenu(self)
1027
1027
1028 self.cut_action = menu.addAction('Cut', self.cut)
1028 self.cut_action = menu.addAction('Cut', self.cut)
1029 self.cut_action.setEnabled(self.can_cut())
1029 self.cut_action.setEnabled(self.can_cut())
1030 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1030 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1031
1031
1032 self.copy_action = menu.addAction('Copy', self.copy)
1032 self.copy_action = menu.addAction('Copy', self.copy)
1033 self.copy_action.setEnabled(self.can_copy())
1033 self.copy_action.setEnabled(self.can_copy())
1034 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1034 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1035
1035
1036 self.paste_action = menu.addAction('Paste', self.paste)
1036 self.paste_action = menu.addAction('Paste', self.paste)
1037 self.paste_action.setEnabled(self.can_paste())
1037 self.paste_action.setEnabled(self.can_paste())
1038 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1038 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1039
1039
1040 anchor = self._control.anchorAt(pos)
1040 anchor = self._control.anchorAt(pos)
1041 if anchor:
1041 if anchor:
1042 menu.addSeparator()
1042 menu.addSeparator()
1043 self.copy_link_action = menu.addAction(
1043 self.copy_link_action = menu.addAction(
1044 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1044 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1045 self.open_link_action = menu.addAction(
1045 self.open_link_action = menu.addAction(
1046 'Open Link', lambda: self.open_anchor(anchor=anchor))
1046 'Open Link', lambda: self.open_anchor(anchor=anchor))
1047
1047
1048 menu.addSeparator()
1048 menu.addSeparator()
1049 menu.addAction(self.select_all_action)
1049 menu.addAction(self.select_all_action)
1050
1050
1051 menu.addSeparator()
1051 menu.addSeparator()
1052 menu.addAction(self.export_action)
1052 menu.addAction(self.export_action)
1053 menu.addAction(self.print_action)
1053 menu.addAction(self.print_action)
1054
1054
1055 return menu
1055 return menu
1056
1056
1057 def _control_key_down(self, modifiers, include_command=False):
1057 def _control_key_down(self, modifiers, include_command=False):
1058 """ Given a KeyboardModifiers flags object, return whether the Control
1058 """ Given a KeyboardModifiers flags object, return whether the Control
1059 key is down.
1059 key is down.
1060
1060
1061 Parameters:
1061 Parameters:
1062 -----------
1062 -----------
1063 include_command : bool, optional (default True)
1063 include_command : bool, optional (default True)
1064 Whether to treat the Command key as a (mutually exclusive) synonym
1064 Whether to treat the Command key as a (mutually exclusive) synonym
1065 for Control when in Mac OS.
1065 for Control when in Mac OS.
1066 """
1066 """
1067 # Note that on Mac OS, ControlModifier corresponds to the Command key
1067 # Note that on Mac OS, ControlModifier corresponds to the Command key
1068 # while MetaModifier corresponds to the Control key.
1068 # while MetaModifier corresponds to the Control key.
1069 if sys.platform == 'darwin':
1069 if sys.platform == 'darwin':
1070 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1070 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1071 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1071 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1072 else:
1072 else:
1073 return bool(modifiers & QtCore.Qt.ControlModifier)
1073 return bool(modifiers & QtCore.Qt.ControlModifier)
1074
1074
1075 def _create_control(self):
1075 def _create_control(self):
1076 """ Creates and connects the underlying text widget.
1076 """ Creates and connects the underlying text widget.
1077 """
1077 """
1078 # Create the underlying control.
1078 # Create the underlying control.
1079 if self.custom_control:
1079 if self.custom_control:
1080 control = self.custom_control()
1080 control = self.custom_control()
1081 elif self.kind == 'plain':
1081 elif self.kind == 'plain':
1082 control = QtGui.QPlainTextEdit()
1082 control = QtGui.QPlainTextEdit()
1083 elif self.kind == 'rich':
1083 elif self.kind == 'rich':
1084 control = QtGui.QTextEdit()
1084 control = QtGui.QTextEdit()
1085 control.setAcceptRichText(False)
1085 control.setAcceptRichText(False)
1086 control.setMouseTracking(True)
1086 control.setMouseTracking(True)
1087
1087
1088 # Prevent the widget from handling drops, as we already provide
1088 # Prevent the widget from handling drops, as we already provide
1089 # the logic in this class.
1089 # the logic in this class.
1090 control.setAcceptDrops(False)
1090 control.setAcceptDrops(False)
1091
1091
1092 # Install event filters. The filter on the viewport is needed for
1092 # Install event filters. The filter on the viewport is needed for
1093 # mouse events.
1093 # mouse events.
1094 control.installEventFilter(self)
1094 control.installEventFilter(self)
1095 control.viewport().installEventFilter(self)
1095 control.viewport().installEventFilter(self)
1096
1096
1097 # Connect signals.
1097 # Connect signals.
1098 control.customContextMenuRequested.connect(
1098 control.customContextMenuRequested.connect(
1099 self._custom_context_menu_requested)
1099 self._custom_context_menu_requested)
1100 control.copyAvailable.connect(self.copy_available)
1100 control.copyAvailable.connect(self.copy_available)
1101 control.redoAvailable.connect(self.redo_available)
1101 control.redoAvailable.connect(self.redo_available)
1102 control.undoAvailable.connect(self.undo_available)
1102 control.undoAvailable.connect(self.undo_available)
1103
1103
1104 # Hijack the document size change signal to prevent Qt from adjusting
1104 # Hijack the document size change signal to prevent Qt from adjusting
1105 # the viewport's scrollbar. We are relying on an implementation detail
1105 # the viewport's scrollbar. We are relying on an implementation detail
1106 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1106 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1107 # this functionality we cannot create a nice terminal interface.
1107 # this functionality we cannot create a nice terminal interface.
1108 layout = control.document().documentLayout()
1108 layout = control.document().documentLayout()
1109 layout.documentSizeChanged.disconnect()
1109 layout.documentSizeChanged.disconnect()
1110 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1110 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1111
1111
1112 # Configure the control.
1112 # Configure the control.
1113 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1113 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1114 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1114 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1115 control.setReadOnly(True)
1115 control.setReadOnly(True)
1116 control.setUndoRedoEnabled(False)
1116 control.setUndoRedoEnabled(False)
1117 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1117 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1118 return control
1118 return control
1119
1119
1120 def _create_page_control(self):
1120 def _create_page_control(self):
1121 """ Creates and connects the underlying paging widget.
1121 """ Creates and connects the underlying paging widget.
1122 """
1122 """
1123 if self.custom_page_control:
1123 if self.custom_page_control:
1124 control = self.custom_page_control()
1124 control = self.custom_page_control()
1125 elif self.kind == 'plain':
1125 elif self.kind == 'plain':
1126 control = QtGui.QPlainTextEdit()
1126 control = QtGui.QPlainTextEdit()
1127 elif self.kind == 'rich':
1127 elif self.kind == 'rich':
1128 control = QtGui.QTextEdit()
1128 control = QtGui.QTextEdit()
1129 control.installEventFilter(self)
1129 control.installEventFilter(self)
1130 viewport = control.viewport()
1130 viewport = control.viewport()
1131 viewport.installEventFilter(self)
1131 viewport.installEventFilter(self)
1132 control.setReadOnly(True)
1132 control.setReadOnly(True)
1133 control.setUndoRedoEnabled(False)
1133 control.setUndoRedoEnabled(False)
1134 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1134 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1135 return control
1135 return control
1136
1136
1137 def _event_filter_console_keypress(self, event):
1137 def _event_filter_console_keypress(self, event):
1138 """ Filter key events for the underlying text widget to create a
1138 """ Filter key events for the underlying text widget to create a
1139 console-like interface.
1139 console-like interface.
1140 """
1140 """
1141 intercepted = False
1141 intercepted = False
1142 cursor = self._control.textCursor()
1142 cursor = self._control.textCursor()
1143 position = cursor.position()
1143 position = cursor.position()
1144 key = event.key()
1144 key = event.key()
1145 ctrl_down = self._control_key_down(event.modifiers())
1145 ctrl_down = self._control_key_down(event.modifiers())
1146 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1146 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1147 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1147 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1148
1148
1149 #------ Special sequences ----------------------------------------------
1149 #------ Special sequences ----------------------------------------------
1150
1150
1151 if event.matches(QtGui.QKeySequence.Copy):
1151 if event.matches(QtGui.QKeySequence.Copy):
1152 self.copy()
1152 self.copy()
1153 intercepted = True
1153 intercepted = True
1154
1154
1155 elif event.matches(QtGui.QKeySequence.Cut):
1155 elif event.matches(QtGui.QKeySequence.Cut):
1156 self.cut()
1156 self.cut()
1157 intercepted = True
1157 intercepted = True
1158
1158
1159 elif event.matches(QtGui.QKeySequence.Paste):
1159 elif event.matches(QtGui.QKeySequence.Paste):
1160 self.paste()
1160 self.paste()
1161 intercepted = True
1161 intercepted = True
1162
1162
1163 #------ Special modifier logic -----------------------------------------
1163 #------ Special modifier logic -----------------------------------------
1164
1164
1165 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1165 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1166 intercepted = True
1166 intercepted = True
1167
1167
1168 # Special handling when tab completing in text mode.
1168 # Special handling when tab completing in text mode.
1169 self._cancel_completion()
1169 self._cancel_completion()
1170
1170
1171 if self._in_buffer(position):
1171 if self._in_buffer(position):
1172 # Special handling when a reading a line of raw input.
1172 # Special handling when a reading a line of raw input.
1173 if self._reading:
1173 if self._reading:
1174 self._append_plain_text('\n')
1174 self._append_plain_text('\n')
1175 self._reading = False
1175 self._reading = False
1176 if self._reading_callback:
1176 if self._reading_callback:
1177 self._reading_callback()
1177 self._reading_callback()
1178
1178
1179 # If the input buffer is a single line or there is only
1179 # If the input buffer is a single line or there is only
1180 # whitespace after the cursor, execute. Otherwise, split the
1180 # whitespace after the cursor, execute. Otherwise, split the
1181 # line with a continuation prompt.
1181 # line with a continuation prompt.
1182 elif not self._executing:
1182 elif not self._executing:
1183 cursor.movePosition(QtGui.QTextCursor.End,
1183 cursor.movePosition(QtGui.QTextCursor.End,
1184 QtGui.QTextCursor.KeepAnchor)
1184 QtGui.QTextCursor.KeepAnchor)
1185 at_end = len(cursor.selectedText().strip()) == 0
1185 at_end = len(cursor.selectedText().strip()) == 0
1186 single_line = (self._get_end_cursor().blockNumber() ==
1186 single_line = (self._get_end_cursor().blockNumber() ==
1187 self._get_prompt_cursor().blockNumber())
1187 self._get_prompt_cursor().blockNumber())
1188 if (at_end or shift_down or single_line) and not ctrl_down:
1188 if (at_end or shift_down or single_line) and not ctrl_down:
1189 self.execute(interactive = not shift_down)
1189 self.execute(interactive = not shift_down)
1190 else:
1190 else:
1191 # Do this inside an edit block for clean undo/redo.
1191 # Do this inside an edit block for clean undo/redo.
1192 cursor.beginEditBlock()
1192 cursor.beginEditBlock()
1193 cursor.setPosition(position)
1193 cursor.setPosition(position)
1194 cursor.insertText('\n')
1194 cursor.insertText('\n')
1195 self._insert_continuation_prompt(cursor)
1195 self._insert_continuation_prompt(cursor)
1196 cursor.endEditBlock()
1196 cursor.endEditBlock()
1197
1197
1198 # Ensure that the whole input buffer is visible.
1198 # Ensure that the whole input buffer is visible.
1199 # FIXME: This will not be usable if the input buffer is
1199 # FIXME: This will not be usable if the input buffer is
1200 # taller than the console widget.
1200 # taller than the console widget.
1201 self._control.moveCursor(QtGui.QTextCursor.End)
1201 self._control.moveCursor(QtGui.QTextCursor.End)
1202 self._control.setTextCursor(cursor)
1202 self._control.setTextCursor(cursor)
1203
1203
1204 #------ Control/Cmd modifier -------------------------------------------
1204 #------ Control/Cmd modifier -------------------------------------------
1205
1205
1206 elif ctrl_down:
1206 elif ctrl_down:
1207 if key == QtCore.Qt.Key_G:
1207 if key == QtCore.Qt.Key_G:
1208 self._keyboard_quit()
1208 self._keyboard_quit()
1209 intercepted = True
1209 intercepted = True
1210
1210
1211 elif key == QtCore.Qt.Key_K:
1211 elif key == QtCore.Qt.Key_K:
1212 if self._in_buffer(position):
1212 if self._in_buffer(position):
1213 cursor.clearSelection()
1213 cursor.clearSelection()
1214 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1214 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1215 QtGui.QTextCursor.KeepAnchor)
1215 QtGui.QTextCursor.KeepAnchor)
1216 if not cursor.hasSelection():
1216 if not cursor.hasSelection():
1217 # Line deletion (remove continuation prompt)
1217 # Line deletion (remove continuation prompt)
1218 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1218 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1219 QtGui.QTextCursor.KeepAnchor)
1219 QtGui.QTextCursor.KeepAnchor)
1220 cursor.movePosition(QtGui.QTextCursor.Right,
1220 cursor.movePosition(QtGui.QTextCursor.Right,
1221 QtGui.QTextCursor.KeepAnchor,
1221 QtGui.QTextCursor.KeepAnchor,
1222 len(self._continuation_prompt))
1222 len(self._continuation_prompt))
1223 self._kill_ring.kill_cursor(cursor)
1223 self._kill_ring.kill_cursor(cursor)
1224 self._set_cursor(cursor)
1224 self._set_cursor(cursor)
1225 intercepted = True
1225 intercepted = True
1226
1226
1227 elif key == QtCore.Qt.Key_L:
1227 elif key == QtCore.Qt.Key_L:
1228 self.prompt_to_top()
1228 self.prompt_to_top()
1229 intercepted = True
1229 intercepted = True
1230
1230
1231 elif key == QtCore.Qt.Key_O:
1231 elif key == QtCore.Qt.Key_O:
1232 if self._page_control and self._page_control.isVisible():
1232 if self._page_control and self._page_control.isVisible():
1233 self._page_control.setFocus()
1233 self._page_control.setFocus()
1234 intercepted = True
1234 intercepted = True
1235
1235
1236 elif key == QtCore.Qt.Key_U:
1236 elif key == QtCore.Qt.Key_U:
1237 if self._in_buffer(position):
1237 if self._in_buffer(position):
1238 cursor.clearSelection()
1238 cursor.clearSelection()
1239 start_line = cursor.blockNumber()
1239 start_line = cursor.blockNumber()
1240 if start_line == self._get_prompt_cursor().blockNumber():
1240 if start_line == self._get_prompt_cursor().blockNumber():
1241 offset = len(self._prompt)
1241 offset = len(self._prompt)
1242 else:
1242 else:
1243 offset = len(self._continuation_prompt)
1243 offset = len(self._continuation_prompt)
1244 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1244 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1245 QtGui.QTextCursor.KeepAnchor)
1245 QtGui.QTextCursor.KeepAnchor)
1246 cursor.movePosition(QtGui.QTextCursor.Right,
1246 cursor.movePosition(QtGui.QTextCursor.Right,
1247 QtGui.QTextCursor.KeepAnchor, offset)
1247 QtGui.QTextCursor.KeepAnchor, offset)
1248 self._kill_ring.kill_cursor(cursor)
1248 self._kill_ring.kill_cursor(cursor)
1249 self._set_cursor(cursor)
1249 self._set_cursor(cursor)
1250 intercepted = True
1250 intercepted = True
1251
1251
1252 elif key == QtCore.Qt.Key_Y:
1252 elif key == QtCore.Qt.Key_Y:
1253 self._keep_cursor_in_buffer()
1253 self._keep_cursor_in_buffer()
1254 self._kill_ring.yank()
1254 self._kill_ring.yank()
1255 intercepted = True
1255 intercepted = True
1256
1256
1257 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1257 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1258 if key == QtCore.Qt.Key_Backspace:
1258 if key == QtCore.Qt.Key_Backspace:
1259 cursor = self._get_word_start_cursor(position)
1259 cursor = self._get_word_start_cursor(position)
1260 else: # key == QtCore.Qt.Key_Delete
1260 else: # key == QtCore.Qt.Key_Delete
1261 cursor = self._get_word_end_cursor(position)
1261 cursor = self._get_word_end_cursor(position)
1262 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1262 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1263 self._kill_ring.kill_cursor(cursor)
1263 self._kill_ring.kill_cursor(cursor)
1264 intercepted = True
1264 intercepted = True
1265
1265
1266 elif key == QtCore.Qt.Key_D:
1266 elif key == QtCore.Qt.Key_D:
1267 if len(self.input_buffer) == 0:
1267 if len(self.input_buffer) == 0:
1268 self.exit_requested.emit(self)
1268 self.exit_requested.emit(self)
1269 else:
1269 else:
1270 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1270 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1271 QtCore.Qt.Key_Delete,
1271 QtCore.Qt.Key_Delete,
1272 QtCore.Qt.NoModifier)
1272 QtCore.Qt.NoModifier)
1273 QtGui.qApp.sendEvent(self._control, new_event)
1273 QtGui.qApp.sendEvent(self._control, new_event)
1274 intercepted = True
1274 intercepted = True
1275
1275
1276 #------ Alt modifier ---------------------------------------------------
1276 #------ Alt modifier ---------------------------------------------------
1277
1277
1278 elif alt_down:
1278 elif alt_down:
1279 if key == QtCore.Qt.Key_B:
1279 if key == QtCore.Qt.Key_B:
1280 self._set_cursor(self._get_word_start_cursor(position))
1280 self._set_cursor(self._get_word_start_cursor(position))
1281 intercepted = True
1281 intercepted = True
1282
1282
1283 elif key == QtCore.Qt.Key_F:
1283 elif key == QtCore.Qt.Key_F:
1284 self._set_cursor(self._get_word_end_cursor(position))
1284 self._set_cursor(self._get_word_end_cursor(position))
1285 intercepted = True
1285 intercepted = True
1286
1286
1287 elif key == QtCore.Qt.Key_Y:
1287 elif key == QtCore.Qt.Key_Y:
1288 self._kill_ring.rotate()
1288 self._kill_ring.rotate()
1289 intercepted = True
1289 intercepted = True
1290
1290
1291 elif key == QtCore.Qt.Key_Backspace:
1291 elif key == QtCore.Qt.Key_Backspace:
1292 cursor = self._get_word_start_cursor(position)
1292 cursor = self._get_word_start_cursor(position)
1293 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1293 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1294 self._kill_ring.kill_cursor(cursor)
1294 self._kill_ring.kill_cursor(cursor)
1295 intercepted = True
1295 intercepted = True
1296
1296
1297 elif key == QtCore.Qt.Key_D:
1297 elif key == QtCore.Qt.Key_D:
1298 cursor = self._get_word_end_cursor(position)
1298 cursor = self._get_word_end_cursor(position)
1299 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1299 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1300 self._kill_ring.kill_cursor(cursor)
1300 self._kill_ring.kill_cursor(cursor)
1301 intercepted = True
1301 intercepted = True
1302
1302
1303 elif key == QtCore.Qt.Key_Delete:
1303 elif key == QtCore.Qt.Key_Delete:
1304 intercepted = True
1304 intercepted = True
1305
1305
1306 elif key == QtCore.Qt.Key_Greater:
1306 elif key == QtCore.Qt.Key_Greater:
1307 self._control.moveCursor(QtGui.QTextCursor.End)
1307 self._control.moveCursor(QtGui.QTextCursor.End)
1308 intercepted = True
1308 intercepted = True
1309
1309
1310 elif key == QtCore.Qt.Key_Less:
1310 elif key == QtCore.Qt.Key_Less:
1311 self._control.setTextCursor(self._get_prompt_cursor())
1311 self._control.setTextCursor(self._get_prompt_cursor())
1312 intercepted = True
1312 intercepted = True
1313
1313
1314 #------ No modifiers ---------------------------------------------------
1314 #------ No modifiers ---------------------------------------------------
1315
1315
1316 else:
1316 else:
1317 if shift_down:
1317 if shift_down:
1318 anchormode = QtGui.QTextCursor.KeepAnchor
1318 anchormode = QtGui.QTextCursor.KeepAnchor
1319 else:
1319 else:
1320 anchormode = QtGui.QTextCursor.MoveAnchor
1320 anchormode = QtGui.QTextCursor.MoveAnchor
1321
1321
1322 if key == QtCore.Qt.Key_Escape:
1322 if key == QtCore.Qt.Key_Escape:
1323 self._keyboard_quit()
1323 self._keyboard_quit()
1324 intercepted = True
1324 intercepted = True
1325
1325
1326 elif key == QtCore.Qt.Key_Up:
1326 elif key == QtCore.Qt.Key_Up:
1327 if self._reading or not self._up_pressed(shift_down):
1327 if self._reading or not self._up_pressed(shift_down):
1328 intercepted = True
1328 intercepted = True
1329 else:
1329 else:
1330 prompt_line = self._get_prompt_cursor().blockNumber()
1330 prompt_line = self._get_prompt_cursor().blockNumber()
1331 intercepted = cursor.blockNumber() <= prompt_line
1331 intercepted = cursor.blockNumber() <= prompt_line
1332
1332
1333 elif key == QtCore.Qt.Key_Down:
1333 elif key == QtCore.Qt.Key_Down:
1334 if self._reading or not self._down_pressed(shift_down):
1334 if self._reading or not self._down_pressed(shift_down):
1335 intercepted = True
1335 intercepted = True
1336 else:
1336 else:
1337 end_line = self._get_end_cursor().blockNumber()
1337 end_line = self._get_end_cursor().blockNumber()
1338 intercepted = cursor.blockNumber() == end_line
1338 intercepted = cursor.blockNumber() == end_line
1339
1339
1340 elif key == QtCore.Qt.Key_Tab:
1340 elif key == QtCore.Qt.Key_Tab:
1341 if not self._reading:
1341 if not self._reading:
1342 if self._tab_pressed():
1342 if self._tab_pressed():
1343 # real tab-key, insert four spaces
1343 # real tab-key, insert four spaces
1344 cursor.insertText(' '*4)
1344 cursor.insertText(' '*4)
1345 intercepted = True
1345 intercepted = True
1346
1346
1347 elif key == QtCore.Qt.Key_Left:
1347 elif key == QtCore.Qt.Key_Left:
1348
1348
1349 # Move to the previous line
1349 # Move to the previous line
1350 line, col = cursor.blockNumber(), cursor.columnNumber()
1350 line, col = cursor.blockNumber(), cursor.columnNumber()
1351 if line > self._get_prompt_cursor().blockNumber() and \
1351 if line > self._get_prompt_cursor().blockNumber() and \
1352 col == len(self._continuation_prompt):
1352 col == len(self._continuation_prompt):
1353 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1353 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1354 mode=anchormode)
1354 mode=anchormode)
1355 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1355 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1356 mode=anchormode)
1356 mode=anchormode)
1357 intercepted = True
1357 intercepted = True
1358
1358
1359 # Regular left movement
1359 # Regular left movement
1360 else:
1360 else:
1361 intercepted = not self._in_buffer(position - 1)
1361 intercepted = not self._in_buffer(position - 1)
1362
1362
1363 elif key == QtCore.Qt.Key_Right:
1363 elif key == QtCore.Qt.Key_Right:
1364 original_block_number = cursor.blockNumber()
1364 original_block_number = cursor.blockNumber()
1365 cursor.movePosition(QtGui.QTextCursor.Right,
1365 cursor.movePosition(QtGui.QTextCursor.Right,
1366 mode=anchormode)
1366 mode=anchormode)
1367 if cursor.blockNumber() != original_block_number:
1367 if cursor.blockNumber() != original_block_number:
1368 cursor.movePosition(QtGui.QTextCursor.Right,
1368 cursor.movePosition(QtGui.QTextCursor.Right,
1369 n=len(self._continuation_prompt),
1369 n=len(self._continuation_prompt),
1370 mode=anchormode)
1370 mode=anchormode)
1371 self._set_cursor(cursor)
1371 self._set_cursor(cursor)
1372 intercepted = True
1372 intercepted = True
1373
1373
1374 elif key == QtCore.Qt.Key_Home:
1374 elif key == QtCore.Qt.Key_Home:
1375 start_line = cursor.blockNumber()
1375 start_line = cursor.blockNumber()
1376 if start_line == self._get_prompt_cursor().blockNumber():
1376 if start_line == self._get_prompt_cursor().blockNumber():
1377 start_pos = self._prompt_pos
1377 start_pos = self._prompt_pos
1378 else:
1378 else:
1379 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1379 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1380 QtGui.QTextCursor.KeepAnchor)
1380 QtGui.QTextCursor.KeepAnchor)
1381 start_pos = cursor.position()
1381 start_pos = cursor.position()
1382 start_pos += len(self._continuation_prompt)
1382 start_pos += len(self._continuation_prompt)
1383 cursor.setPosition(position)
1383 cursor.setPosition(position)
1384 if shift_down and self._in_buffer(position):
1384 if shift_down and self._in_buffer(position):
1385 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1385 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1386 else:
1386 else:
1387 cursor.setPosition(start_pos)
1387 cursor.setPosition(start_pos)
1388 self._set_cursor(cursor)
1388 self._set_cursor(cursor)
1389 intercepted = True
1389 intercepted = True
1390
1390
1391 elif key == QtCore.Qt.Key_Backspace:
1391 elif key == QtCore.Qt.Key_Backspace:
1392
1392
1393 # Line deletion (remove continuation prompt)
1393 # Line deletion (remove continuation prompt)
1394 line, col = cursor.blockNumber(), cursor.columnNumber()
1394 line, col = cursor.blockNumber(), cursor.columnNumber()
1395 if not self._reading and \
1395 if not self._reading and \
1396 col == len(self._continuation_prompt) and \
1396 col == len(self._continuation_prompt) and \
1397 line > self._get_prompt_cursor().blockNumber():
1397 line > self._get_prompt_cursor().blockNumber():
1398 cursor.beginEditBlock()
1398 cursor.beginEditBlock()
1399 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1399 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1400 QtGui.QTextCursor.KeepAnchor)
1400 QtGui.QTextCursor.KeepAnchor)
1401 cursor.removeSelectedText()
1401 cursor.removeSelectedText()
1402 cursor.deletePreviousChar()
1402 cursor.deletePreviousChar()
1403 cursor.endEditBlock()
1403 cursor.endEditBlock()
1404 intercepted = True
1404 intercepted = True
1405
1405
1406 # Regular backwards deletion
1406 # Regular backwards deletion
1407 else:
1407 else:
1408 anchor = cursor.anchor()
1408 anchor = cursor.anchor()
1409 if anchor == position:
1409 if anchor == position:
1410 intercepted = not self._in_buffer(position - 1)
1410 intercepted = not self._in_buffer(position - 1)
1411 else:
1411 else:
1412 intercepted = not self._in_buffer(min(anchor, position))
1412 intercepted = not self._in_buffer(min(anchor, position))
1413
1413
1414 elif key == QtCore.Qt.Key_Delete:
1414 elif key == QtCore.Qt.Key_Delete:
1415
1415
1416 # Line deletion (remove continuation prompt)
1416 # Line deletion (remove continuation prompt)
1417 if not self._reading and self._in_buffer(position) and \
1417 if not self._reading and self._in_buffer(position) and \
1418 cursor.atBlockEnd() and not cursor.hasSelection():
1418 cursor.atBlockEnd() and not cursor.hasSelection():
1419 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1419 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1420 QtGui.QTextCursor.KeepAnchor)
1420 QtGui.QTextCursor.KeepAnchor)
1421 cursor.movePosition(QtGui.QTextCursor.Right,
1421 cursor.movePosition(QtGui.QTextCursor.Right,
1422 QtGui.QTextCursor.KeepAnchor,
1422 QtGui.QTextCursor.KeepAnchor,
1423 len(self._continuation_prompt))
1423 len(self._continuation_prompt))
1424 cursor.removeSelectedText()
1424 cursor.removeSelectedText()
1425 intercepted = True
1425 intercepted = True
1426
1426
1427 # Regular forwards deletion:
1427 # Regular forwards deletion:
1428 else:
1428 else:
1429 anchor = cursor.anchor()
1429 anchor = cursor.anchor()
1430 intercepted = (not self._in_buffer(anchor) or
1430 intercepted = (not self._in_buffer(anchor) or
1431 not self._in_buffer(position))
1431 not self._in_buffer(position))
1432
1432
1433 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1433 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1434 # using the keyboard in any part of the buffer. Also, permit scrolling
1434 # using the keyboard in any part of the buffer. Also, permit scrolling
1435 # with Page Up/Down keys. Finally, if we're executing, don't move the
1435 # with Page Up/Down keys. Finally, if we're executing, don't move the
1436 # cursor (if even this made sense, we can't guarantee that the prompt
1436 # cursor (if even this made sense, we can't guarantee that the prompt
1437 # position is still valid due to text truncation).
1437 # position is still valid due to text truncation).
1438 if not (self._control_key_down(event.modifiers(), include_command=True)
1438 if not (self._control_key_down(event.modifiers(), include_command=True)
1439 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1439 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1440 or (self._executing and not self._reading)):
1440 or (self._executing and not self._reading)):
1441 self._keep_cursor_in_buffer()
1441 self._keep_cursor_in_buffer()
1442
1442
1443 return intercepted
1443 return intercepted
1444
1444
1445 def _event_filter_page_keypress(self, event):
1445 def _event_filter_page_keypress(self, event):
1446 """ Filter key events for the paging widget to create console-like
1446 """ Filter key events for the paging widget to create console-like
1447 interface.
1447 interface.
1448 """
1448 """
1449 key = event.key()
1449 key = event.key()
1450 ctrl_down = self._control_key_down(event.modifiers())
1450 ctrl_down = self._control_key_down(event.modifiers())
1451 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1451 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1452
1452
1453 if ctrl_down:
1453 if ctrl_down:
1454 if key == QtCore.Qt.Key_O:
1454 if key == QtCore.Qt.Key_O:
1455 self._control.setFocus()
1455 self._control.setFocus()
1456 intercept = True
1456 intercept = True
1457
1457
1458 elif alt_down:
1458 elif alt_down:
1459 if key == QtCore.Qt.Key_Greater:
1459 if key == QtCore.Qt.Key_Greater:
1460 self._page_control.moveCursor(QtGui.QTextCursor.End)
1460 self._page_control.moveCursor(QtGui.QTextCursor.End)
1461 intercepted = True
1461 intercepted = True
1462
1462
1463 elif key == QtCore.Qt.Key_Less:
1463 elif key == QtCore.Qt.Key_Less:
1464 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1464 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1465 intercepted = True
1465 intercepted = True
1466
1466
1467 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1467 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1468 if self._splitter:
1468 if self._splitter:
1469 self._page_control.hide()
1469 self._page_control.hide()
1470 self._control.setFocus()
1470 self._control.setFocus()
1471 else:
1471 else:
1472 self.layout().setCurrentWidget(self._control)
1472 self.layout().setCurrentWidget(self._control)
1473 return True
1473 return True
1474
1474
1475 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1475 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1476 QtCore.Qt.Key_Tab):
1476 QtCore.Qt.Key_Tab):
1477 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1477 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1478 QtCore.Qt.Key_PageDown,
1478 QtCore.Qt.Key_PageDown,
1479 QtCore.Qt.NoModifier)
1479 QtCore.Qt.NoModifier)
1480 QtGui.qApp.sendEvent(self._page_control, new_event)
1480 QtGui.qApp.sendEvent(self._page_control, new_event)
1481 return True
1481 return True
1482
1482
1483 elif key == QtCore.Qt.Key_Backspace:
1483 elif key == QtCore.Qt.Key_Backspace:
1484 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1484 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1485 QtCore.Qt.Key_PageUp,
1485 QtCore.Qt.Key_PageUp,
1486 QtCore.Qt.NoModifier)
1486 QtCore.Qt.NoModifier)
1487 QtGui.qApp.sendEvent(self._page_control, new_event)
1487 QtGui.qApp.sendEvent(self._page_control, new_event)
1488 return True
1488 return True
1489
1489
1490 return False
1490 return False
1491
1491
1492 def _flush_pending_stream(self):
1492 def _flush_pending_stream(self):
1493 """ Flush out pending text into the widget. """
1493 """ Flush out pending text into the widget. """
1494 text = self._pending_insert_text
1494 text = self._pending_insert_text
1495 self._pending_insert_text = []
1495 self._pending_insert_text = []
1496 buffer_size = self._control.document().maximumBlockCount()
1496 buffer_size = self._control.document().maximumBlockCount()
1497 if buffer_size > 0:
1497 if buffer_size > 0:
1498 text = self._get_last_lines_from_list(text, buffer_size)
1498 text = self._get_last_lines_from_list(text, buffer_size)
1499 text = ''.join(text)
1499 text = ''.join(text)
1500 t = time.time()
1500 t = time.time()
1501 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1501 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1502 # Set the flush interval to equal the maximum time to update text.
1502 # Set the flush interval to equal the maximum time to update text.
1503 self._pending_text_flush_interval.setInterval(max(100,
1503 self._pending_text_flush_interval.setInterval(max(100,
1504 (time.time()-t)*1000))
1504 (time.time()-t)*1000))
1505
1505
1506 def _format_as_columns(self, items, separator=' '):
1506 def _format_as_columns(self, items, separator=' '):
1507 """ Transform a list of strings into a single string with columns.
1507 """ Transform a list of strings into a single string with columns.
1508
1508
1509 Parameters
1509 Parameters
1510 ----------
1510 ----------
1511 items : sequence of strings
1511 items : sequence of strings
1512 The strings to process.
1512 The strings to process.
1513
1513
1514 separator : str, optional [default is two spaces]
1514 separator : str, optional [default is two spaces]
1515 The string that separates columns.
1515 The string that separates columns.
1516
1516
1517 Returns
1517 Returns
1518 -------
1518 -------
1519 The formatted string.
1519 The formatted string.
1520 """
1520 """
1521 # Calculate the number of characters available.
1521 # Calculate the number of characters available.
1522 width = self._control.viewport().width()
1522 width = self._control.viewport().width()
1523 char_width = QtGui.QFontMetrics(self.font).width(' ')
1523 char_width = QtGui.QFontMetrics(self.font).width(' ')
1524 displaywidth = max(10, (width / char_width) - 1)
1524 displaywidth = max(10, (width / char_width) - 1)
1525
1525
1526 return columnize(items, separator, displaywidth)
1526 return columnize(items, separator, displaywidth)
1527
1527
1528 def _get_block_plain_text(self, block):
1528 def _get_block_plain_text(self, block):
1529 """ Given a QTextBlock, return its unformatted text.
1529 """ Given a QTextBlock, return its unformatted text.
1530 """
1530 """
1531 cursor = QtGui.QTextCursor(block)
1531 cursor = QtGui.QTextCursor(block)
1532 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1532 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1533 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1533 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1534 QtGui.QTextCursor.KeepAnchor)
1534 QtGui.QTextCursor.KeepAnchor)
1535 return cursor.selection().toPlainText()
1535 return cursor.selection().toPlainText()
1536
1536
1537 def _get_cursor(self):
1537 def _get_cursor(self):
1538 """ Convenience method that returns a cursor for the current position.
1538 """ Convenience method that returns a cursor for the current position.
1539 """
1539 """
1540 return self._control.textCursor()
1540 return self._control.textCursor()
1541
1541
1542 def _get_end_cursor(self):
1542 def _get_end_cursor(self):
1543 """ Convenience method that returns a cursor for the last character.
1543 """ Convenience method that returns a cursor for the last character.
1544 """
1544 """
1545 cursor = self._control.textCursor()
1545 cursor = self._control.textCursor()
1546 cursor.movePosition(QtGui.QTextCursor.End)
1546 cursor.movePosition(QtGui.QTextCursor.End)
1547 return cursor
1547 return cursor
1548
1548
1549 def _get_input_buffer_cursor_column(self):
1549 def _get_input_buffer_cursor_column(self):
1550 """ Returns the column of the cursor in the input buffer, excluding the
1550 """ Returns the column of the cursor in the input buffer, excluding the
1551 contribution by the prompt, or -1 if there is no such column.
1551 contribution by the prompt, or -1 if there is no such column.
1552 """
1552 """
1553 prompt = self._get_input_buffer_cursor_prompt()
1553 prompt = self._get_input_buffer_cursor_prompt()
1554 if prompt is None:
1554 if prompt is None:
1555 return -1
1555 return -1
1556 else:
1556 else:
1557 cursor = self._control.textCursor()
1557 cursor = self._control.textCursor()
1558 return cursor.columnNumber() - len(prompt)
1558 return cursor.columnNumber() - len(prompt)
1559
1559
1560 def _get_input_buffer_cursor_line(self):
1560 def _get_input_buffer_cursor_line(self):
1561 """ Returns the text of the line of the input buffer that contains the
1561 """ Returns the text of the line of the input buffer that contains the
1562 cursor, or None if there is no such line.
1562 cursor, or None if there is no such line.
1563 """
1563 """
1564 prompt = self._get_input_buffer_cursor_prompt()
1564 prompt = self._get_input_buffer_cursor_prompt()
1565 if prompt is None:
1565 if prompt is None:
1566 return None
1566 return None
1567 else:
1567 else:
1568 cursor = self._control.textCursor()
1568 cursor = self._control.textCursor()
1569 text = self._get_block_plain_text(cursor.block())
1569 text = self._get_block_plain_text(cursor.block())
1570 return text[len(prompt):]
1570 return text[len(prompt):]
1571
1571
1572 def _get_input_buffer_cursor_prompt(self):
1572 def _get_input_buffer_cursor_prompt(self):
1573 """ Returns the (plain text) prompt for line of the input buffer that
1573 """ Returns the (plain text) prompt for line of the input buffer that
1574 contains the cursor, or None if there is no such line.
1574 contains the cursor, or None if there is no such line.
1575 """
1575 """
1576 if self._executing:
1576 if self._executing:
1577 return None
1577 return None
1578 cursor = self._control.textCursor()
1578 cursor = self._control.textCursor()
1579 if cursor.position() >= self._prompt_pos:
1579 if cursor.position() >= self._prompt_pos:
1580 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1580 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1581 return self._prompt
1581 return self._prompt
1582 else:
1582 else:
1583 return self._continuation_prompt
1583 return self._continuation_prompt
1584 else:
1584 else:
1585 return None
1585 return None
1586
1586
1587 def _get_last_lines(self, text, num_lines, return_count=False):
1587 def _get_last_lines(self, text, num_lines, return_count=False):
1588 """ Return last specified number of lines of text (like `tail -n`).
1588 """ Return last specified number of lines of text (like `tail -n`).
1589 If return_count is True, returns a tuple of clipped text and the
1589 If return_count is True, returns a tuple of clipped text and the
1590 number of lines in the clipped text.
1590 number of lines in the clipped text.
1591 """
1591 """
1592 pos = len(text)
1592 pos = len(text)
1593 if pos < num_lines:
1593 if pos < num_lines:
1594 if return_count:
1594 if return_count:
1595 return text, text.count('\n') if return_count else text
1595 return text, text.count('\n') if return_count else text
1596 else:
1596 else:
1597 return text
1597 return text
1598 i = 0
1598 i = 0
1599 while i < num_lines:
1599 while i < num_lines:
1600 pos = text.rfind('\n', None, pos)
1600 pos = text.rfind('\n', None, pos)
1601 if pos == -1:
1601 if pos == -1:
1602 pos = None
1602 pos = None
1603 break
1603 break
1604 i += 1
1604 i += 1
1605 if return_count:
1605 if return_count:
1606 return text[pos:], i
1606 return text[pos:], i
1607 else:
1607 else:
1608 return text[pos:]
1608 return text[pos:]
1609
1609
1610 def _get_last_lines_from_list(self, text_list, num_lines):
1610 def _get_last_lines_from_list(self, text_list, num_lines):
1611 """ Return the list of text clipped to last specified lines.
1611 """ Return the list of text clipped to last specified lines.
1612 """
1612 """
1613 ret = []
1613 ret = []
1614 lines_pending = num_lines
1614 lines_pending = num_lines
1615 for text in reversed(text_list):
1615 for text in reversed(text_list):
1616 text, lines_added = self._get_last_lines(text, lines_pending,
1616 text, lines_added = self._get_last_lines(text, lines_pending,
1617 return_count=True)
1617 return_count=True)
1618 ret.append(text)
1618 ret.append(text)
1619 lines_pending -= lines_added
1619 lines_pending -= lines_added
1620 if lines_pending <= 0:
1620 if lines_pending <= 0:
1621 break
1621 break
1622 return ret[::-1]
1622 return ret[::-1]
1623
1623
1624 def _get_prompt_cursor(self):
1624 def _get_prompt_cursor(self):
1625 """ Convenience method that returns a cursor for the prompt position.
1625 """ Convenience method that returns a cursor for the prompt position.
1626 """
1626 """
1627 cursor = self._control.textCursor()
1627 cursor = self._control.textCursor()
1628 cursor.setPosition(self._prompt_pos)
1628 cursor.setPosition(self._prompt_pos)
1629 return cursor
1629 return cursor
1630
1630
1631 def _get_selection_cursor(self, start, end):
1631 def _get_selection_cursor(self, start, end):
1632 """ Convenience method that returns a cursor with text selected between
1632 """ Convenience method that returns a cursor with text selected between
1633 the positions 'start' and 'end'.
1633 the positions 'start' and 'end'.
1634 """
1634 """
1635 cursor = self._control.textCursor()
1635 cursor = self._control.textCursor()
1636 cursor.setPosition(start)
1636 cursor.setPosition(start)
1637 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1637 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1638 return cursor
1638 return cursor
1639
1639
1640 def _get_word_start_cursor(self, position):
1640 def _get_word_start_cursor(self, position):
1641 """ Find the start of the word to the left the given position. If a
1641 """ Find the start of the word to the left the given position. If a
1642 sequence of non-word characters precedes the first word, skip over
1642 sequence of non-word characters precedes the first word, skip over
1643 them. (This emulates the behavior of bash, emacs, etc.)
1643 them. (This emulates the behavior of bash, emacs, etc.)
1644 """
1644 """
1645 document = self._control.document()
1645 document = self._control.document()
1646 position -= 1
1646 position -= 1
1647 while position >= self._prompt_pos and \
1647 while position >= self._prompt_pos and \
1648 not is_letter_or_number(document.characterAt(position)):
1648 not is_letter_or_number(document.characterAt(position)):
1649 position -= 1
1649 position -= 1
1650 while position >= self._prompt_pos and \
1650 while position >= self._prompt_pos and \
1651 is_letter_or_number(document.characterAt(position)):
1651 is_letter_or_number(document.characterAt(position)):
1652 position -= 1
1652 position -= 1
1653 cursor = self._control.textCursor()
1653 cursor = self._control.textCursor()
1654 cursor.setPosition(position + 1)
1654 cursor.setPosition(position + 1)
1655 return cursor
1655 return cursor
1656
1656
1657 def _get_word_end_cursor(self, position):
1657 def _get_word_end_cursor(self, position):
1658 """ Find the end of the word to the right the given position. If a
1658 """ Find the end of the word to the right the given position. If a
1659 sequence of non-word characters precedes the first word, skip over
1659 sequence of non-word characters precedes the first word, skip over
1660 them. (This emulates the behavior of bash, emacs, etc.)
1660 them. (This emulates the behavior of bash, emacs, etc.)
1661 """
1661 """
1662 document = self._control.document()
1662 document = self._control.document()
1663 end = self._get_end_cursor().position()
1663 end = self._get_end_cursor().position()
1664 while position < end and \
1664 while position < end and \
1665 not is_letter_or_number(document.characterAt(position)):
1665 not is_letter_or_number(document.characterAt(position)):
1666 position += 1
1666 position += 1
1667 while position < end and \
1667 while position < end and \
1668 is_letter_or_number(document.characterAt(position)):
1668 is_letter_or_number(document.characterAt(position)):
1669 position += 1
1669 position += 1
1670 cursor = self._control.textCursor()
1670 cursor = self._control.textCursor()
1671 cursor.setPosition(position)
1671 cursor.setPosition(position)
1672 return cursor
1672 return cursor
1673
1673
1674 def _insert_continuation_prompt(self, cursor):
1674 def _insert_continuation_prompt(self, cursor):
1675 """ Inserts new continuation prompt using the specified cursor.
1675 """ Inserts new continuation prompt using the specified cursor.
1676 """
1676 """
1677 if self._continuation_prompt_html is None:
1677 if self._continuation_prompt_html is None:
1678 self._insert_plain_text(cursor, self._continuation_prompt)
1678 self._insert_plain_text(cursor, self._continuation_prompt)
1679 else:
1679 else:
1680 self._continuation_prompt = self._insert_html_fetching_plain_text(
1680 self._continuation_prompt = self._insert_html_fetching_plain_text(
1681 cursor, self._continuation_prompt_html)
1681 cursor, self._continuation_prompt_html)
1682
1682
1683 def _insert_block(self, cursor, block_format=None):
1683 def _insert_block(self, cursor, block_format=None):
1684 """ Inserts an empty QTextBlock using the specified cursor.
1684 """ Inserts an empty QTextBlock using the specified cursor.
1685 """
1685 """
1686 if block_format is None:
1686 if block_format is None:
1687 block_format = QtGui.QTextBlockFormat()
1687 block_format = QtGui.QTextBlockFormat()
1688 cursor.insertBlock(block_format)
1688 cursor.insertBlock(block_format)
1689
1689
1690 def _insert_html(self, cursor, html):
1690 def _insert_html(self, cursor, html):
1691 """ Inserts HTML using the specified cursor in such a way that future
1691 """ Inserts HTML using the specified cursor in such a way that future
1692 formatting is unaffected.
1692 formatting is unaffected.
1693 """
1693 """
1694 cursor.beginEditBlock()
1694 cursor.beginEditBlock()
1695 cursor.insertHtml(html)
1695 cursor.insertHtml(html)
1696
1696
1697 # After inserting HTML, the text document "remembers" it's in "html
1697 # After inserting HTML, the text document "remembers" it's in "html
1698 # mode", which means that subsequent calls adding plain text will result
1698 # mode", which means that subsequent calls adding plain text will result
1699 # in unwanted formatting, lost tab characters, etc. The following code
1699 # in unwanted formatting, lost tab characters, etc. The following code
1700 # hacks around this behavior, which I consider to be a bug in Qt, by
1700 # hacks around this behavior, which I consider to be a bug in Qt, by
1701 # (crudely) resetting the document's style state.
1701 # (crudely) resetting the document's style state.
1702 cursor.movePosition(QtGui.QTextCursor.Left,
1702 cursor.movePosition(QtGui.QTextCursor.Left,
1703 QtGui.QTextCursor.KeepAnchor)
1703 QtGui.QTextCursor.KeepAnchor)
1704 if cursor.selection().toPlainText() == ' ':
1704 if cursor.selection().toPlainText() == ' ':
1705 cursor.removeSelectedText()
1705 cursor.removeSelectedText()
1706 else:
1706 else:
1707 cursor.movePosition(QtGui.QTextCursor.Right)
1707 cursor.movePosition(QtGui.QTextCursor.Right)
1708 cursor.insertText(' ', QtGui.QTextCharFormat())
1708 cursor.insertText(' ', QtGui.QTextCharFormat())
1709 cursor.endEditBlock()
1709 cursor.endEditBlock()
1710
1710
1711 def _insert_html_fetching_plain_text(self, cursor, html):
1711 def _insert_html_fetching_plain_text(self, cursor, html):
1712 """ Inserts HTML using the specified cursor, then returns its plain text
1712 """ Inserts HTML using the specified cursor, then returns its plain text
1713 version.
1713 version.
1714 """
1714 """
1715 cursor.beginEditBlock()
1715 cursor.beginEditBlock()
1716 cursor.removeSelectedText()
1716 cursor.removeSelectedText()
1717
1717
1718 start = cursor.position()
1718 start = cursor.position()
1719 self._insert_html(cursor, html)
1719 self._insert_html(cursor, html)
1720 end = cursor.position()
1720 end = cursor.position()
1721 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1721 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1722 text = cursor.selection().toPlainText()
1722 text = cursor.selection().toPlainText()
1723
1723
1724 cursor.setPosition(end)
1724 cursor.setPosition(end)
1725 cursor.endEditBlock()
1725 cursor.endEditBlock()
1726 return text
1726 return text
1727
1727
1728 def _insert_plain_text(self, cursor, text, flush=False):
1728 def _insert_plain_text(self, cursor, text, flush=False):
1729 """ Inserts plain text using the specified cursor, processing ANSI codes
1729 """ Inserts plain text using the specified cursor, processing ANSI codes
1730 if enabled.
1730 if enabled.
1731 """
1731 """
1732 # maximumBlockCount() can be different from self.buffer_size in
1732 # maximumBlockCount() can be different from self.buffer_size in
1733 # case input prompt is active.
1733 # case input prompt is active.
1734 buffer_size = self._control.document().maximumBlockCount()
1734 buffer_size = self._control.document().maximumBlockCount()
1735
1735
1736 if self._executing and not flush and \
1736 if self._executing and not flush and \
1737 self._pending_text_flush_interval.isActive():
1737 self._pending_text_flush_interval.isActive():
1738 self._pending_insert_text.append(text)
1738 self._pending_insert_text.append(text)
1739 if buffer_size > 0:
1739 if buffer_size > 0:
1740 self._pending_insert_text = self._get_last_lines_from_list(
1740 self._pending_insert_text = self._get_last_lines_from_list(
1741 self._pending_insert_text, buffer_size)
1741 self._pending_insert_text, buffer_size)
1742 return
1742 return
1743
1743
1744 if self._executing and not self._pending_text_flush_interval.isActive():
1744 if self._executing and not self._pending_text_flush_interval.isActive():
1745 self._pending_text_flush_interval.start()
1745 self._pending_text_flush_interval.start()
1746
1746
1747 # Clip the text to last `buffer_size` lines.
1747 # Clip the text to last `buffer_size` lines.
1748 if buffer_size > 0:
1748 if buffer_size > 0:
1749 text = self._get_last_lines(text, buffer_size)
1749 text = self._get_last_lines(text, buffer_size)
1750
1750
1751 cursor.beginEditBlock()
1751 cursor.beginEditBlock()
1752 if self.ansi_codes:
1752 if self.ansi_codes:
1753 for substring in self._ansi_processor.split_string(text):
1753 for substring in self._ansi_processor.split_string(text):
1754 for act in self._ansi_processor.actions:
1754 for act in self._ansi_processor.actions:
1755
1755
1756 # Unlike real terminal emulators, we don't distinguish
1756 # Unlike real terminal emulators, we don't distinguish
1757 # between the screen and the scrollback buffer. A screen
1757 # between the screen and the scrollback buffer. A screen
1758 # erase request clears everything.
1758 # erase request clears everything.
1759 if act.action == 'erase' and act.area == 'screen':
1759 if act.action == 'erase' and act.area == 'screen':
1760 cursor.select(QtGui.QTextCursor.Document)
1760 cursor.select(QtGui.QTextCursor.Document)
1761 cursor.removeSelectedText()
1761 cursor.removeSelectedText()
1762
1762
1763 # Simulate a form feed by scrolling just past the last line.
1763 # Simulate a form feed by scrolling just past the last line.
1764 elif act.action == 'scroll' and act.unit == 'page':
1764 elif act.action == 'scroll' and act.unit == 'page':
1765 cursor.insertText('\n')
1765 cursor.insertText('\n')
1766 cursor.endEditBlock()
1766 cursor.endEditBlock()
1767 self._set_top_cursor(cursor)
1767 self._set_top_cursor(cursor)
1768 cursor.joinPreviousEditBlock()
1768 cursor.joinPreviousEditBlock()
1769 cursor.deletePreviousChar()
1769 cursor.deletePreviousChar()
1770
1770
1771 elif act.action == 'carriage-return':
1771 elif act.action == 'carriage-return':
1772 cursor.movePosition(
1772 cursor.movePosition(
1773 cursor.StartOfLine, cursor.KeepAnchor)
1773 cursor.StartOfLine, cursor.KeepAnchor)
1774
1774
1775 elif act.action == 'beep':
1775 elif act.action == 'beep':
1776 QtGui.qApp.beep()
1776 QtGui.qApp.beep()
1777
1777
1778 elif act.action == 'backspace':
1778 elif act.action == 'backspace':
1779 if not cursor.atBlockStart():
1779 if not cursor.atBlockStart():
1780 cursor.movePosition(
1780 cursor.movePosition(
1781 cursor.PreviousCharacter, cursor.KeepAnchor)
1781 cursor.PreviousCharacter, cursor.KeepAnchor)
1782
1782
1783 elif act.action == 'newline':
1783 elif act.action == 'newline':
1784 cursor.movePosition(cursor.EndOfLine)
1784 cursor.movePosition(cursor.EndOfLine)
1785
1785
1786 format = self._ansi_processor.get_format()
1786 format = self._ansi_processor.get_format()
1787
1787
1788 selection = cursor.selectedText()
1788 selection = cursor.selectedText()
1789 if len(selection) == 0:
1789 if len(selection) == 0:
1790 cursor.insertText(substring, format)
1790 cursor.insertText(substring, format)
1791 elif substring is not None:
1791 elif substring is not None:
1792 # BS and CR are treated as a change in print
1792 # BS and CR are treated as a change in print
1793 # position, rather than a backwards character
1793 # position, rather than a backwards character
1794 # deletion for output equivalence with (I)Python
1794 # deletion for output equivalence with (I)Python
1795 # terminal.
1795 # terminal.
1796 if len(substring) >= len(selection):
1796 if len(substring) >= len(selection):
1797 cursor.insertText(substring, format)
1797 cursor.insertText(substring, format)
1798 else:
1798 else:
1799 old_text = selection[len(substring):]
1799 old_text = selection[len(substring):]
1800 cursor.insertText(substring + old_text, format)
1800 cursor.insertText(substring + old_text, format)
1801 cursor.movePosition(cursor.PreviousCharacter,
1801 cursor.movePosition(cursor.PreviousCharacter,
1802 cursor.KeepAnchor, len(old_text))
1802 cursor.KeepAnchor, len(old_text))
1803 else:
1803 else:
1804 cursor.insertText(text)
1804 cursor.insertText(text)
1805 cursor.endEditBlock()
1805 cursor.endEditBlock()
1806
1806
1807 def _insert_plain_text_into_buffer(self, cursor, text):
1807 def _insert_plain_text_into_buffer(self, cursor, text):
1808 """ Inserts text into the input buffer using the specified cursor (which
1808 """ Inserts text into the input buffer using the specified cursor (which
1809 must be in the input buffer), ensuring that continuation prompts are
1809 must be in the input buffer), ensuring that continuation prompts are
1810 inserted as necessary.
1810 inserted as necessary.
1811 """
1811 """
1812 lines = text.splitlines(True)
1812 lines = text.splitlines(True)
1813 if lines:
1813 if lines:
1814 cursor.beginEditBlock()
1814 cursor.beginEditBlock()
1815 cursor.insertText(lines[0])
1815 cursor.insertText(lines[0])
1816 for line in lines[1:]:
1816 for line in lines[1:]:
1817 if self._continuation_prompt_html is None:
1817 if self._continuation_prompt_html is None:
1818 cursor.insertText(self._continuation_prompt)
1818 cursor.insertText(self._continuation_prompt)
1819 else:
1819 else:
1820 self._continuation_prompt = \
1820 self._continuation_prompt = \
1821 self._insert_html_fetching_plain_text(
1821 self._insert_html_fetching_plain_text(
1822 cursor, self._continuation_prompt_html)
1822 cursor, self._continuation_prompt_html)
1823 cursor.insertText(line)
1823 cursor.insertText(line)
1824 cursor.endEditBlock()
1824 cursor.endEditBlock()
1825
1825
1826 def _in_buffer(self, position=None):
1826 def _in_buffer(self, position=None):
1827 """ Returns whether the current cursor (or, if specified, a position) is
1827 """ Returns whether the current cursor (or, if specified, a position) is
1828 inside the editing region.
1828 inside the editing region.
1829 """
1829 """
1830 cursor = self._control.textCursor()
1830 cursor = self._control.textCursor()
1831 if position is None:
1831 if position is None:
1832 position = cursor.position()
1832 position = cursor.position()
1833 else:
1833 else:
1834 cursor.setPosition(position)
1834 cursor.setPosition(position)
1835 line = cursor.blockNumber()
1835 line = cursor.blockNumber()
1836 prompt_line = self._get_prompt_cursor().blockNumber()
1836 prompt_line = self._get_prompt_cursor().blockNumber()
1837 if line == prompt_line:
1837 if line == prompt_line:
1838 return position >= self._prompt_pos
1838 return position >= self._prompt_pos
1839 elif line > prompt_line:
1839 elif line > prompt_line:
1840 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1840 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1841 prompt_pos = cursor.position() + len(self._continuation_prompt)
1841 prompt_pos = cursor.position() + len(self._continuation_prompt)
1842 return position >= prompt_pos
1842 return position >= prompt_pos
1843 return False
1843 return False
1844
1844
1845 def _keep_cursor_in_buffer(self):
1845 def _keep_cursor_in_buffer(self):
1846 """ Ensures that the cursor is inside the editing region. Returns
1846 """ Ensures that the cursor is inside the editing region. Returns
1847 whether the cursor was moved.
1847 whether the cursor was moved.
1848 """
1848 """
1849 moved = not self._in_buffer()
1849 moved = not self._in_buffer()
1850 if moved:
1850 if moved:
1851 cursor = self._control.textCursor()
1851 cursor = self._control.textCursor()
1852 cursor.movePosition(QtGui.QTextCursor.End)
1852 cursor.movePosition(QtGui.QTextCursor.End)
1853 self._control.setTextCursor(cursor)
1853 self._control.setTextCursor(cursor)
1854 return moved
1854 return moved
1855
1855
1856 def _keyboard_quit(self):
1856 def _keyboard_quit(self):
1857 """ Cancels the current editing task ala Ctrl-G in Emacs.
1857 """ Cancels the current editing task ala Ctrl-G in Emacs.
1858 """
1858 """
1859 if self._temp_buffer_filled :
1859 if self._temp_buffer_filled :
1860 self._cancel_completion()
1860 self._cancel_completion()
1861 self._clear_temporary_buffer()
1861 self._clear_temporary_buffer()
1862 else:
1862 else:
1863 self.input_buffer = ''
1863 self.input_buffer = ''
1864
1864
1865 def _page(self, text, html=False):
1865 def _page(self, text, html=False):
1866 """ Displays text using the pager if it exceeds the height of the
1866 """ Displays text using the pager if it exceeds the height of the
1867 viewport.
1867 viewport.
1868
1868
1869 Parameters:
1869 Parameters:
1870 -----------
1870 -----------
1871 html : bool, optional (default False)
1871 html : bool, optional (default False)
1872 If set, the text will be interpreted as HTML instead of plain text.
1872 If set, the text will be interpreted as HTML instead of plain text.
1873 """
1873 """
1874 line_height = QtGui.QFontMetrics(self.font).height()
1874 line_height = QtGui.QFontMetrics(self.font).height()
1875 minlines = self._control.viewport().height() / line_height
1875 minlines = self._control.viewport().height() / line_height
1876 if self.paging != 'none' and \
1876 if self.paging != 'none' and \
1877 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1877 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1878 if self.paging == 'custom':
1878 if self.paging == 'custom':
1879 self.custom_page_requested.emit(text)
1879 self.custom_page_requested.emit(text)
1880 else:
1880 else:
1881 self._page_control.clear()
1881 self._page_control.clear()
1882 cursor = self._page_control.textCursor()
1882 cursor = self._page_control.textCursor()
1883 if html:
1883 if html:
1884 self._insert_html(cursor, text)
1884 self._insert_html(cursor, text)
1885 else:
1885 else:
1886 self._insert_plain_text(cursor, text)
1886 self._insert_plain_text(cursor, text)
1887 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1887 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1888
1888
1889 self._page_control.viewport().resize(self._control.size())
1889 self._page_control.viewport().resize(self._control.size())
1890 if self._splitter:
1890 if self._splitter:
1891 self._page_control.show()
1891 self._page_control.show()
1892 self._page_control.setFocus()
1892 self._page_control.setFocus()
1893 else:
1893 else:
1894 self.layout().setCurrentWidget(self._page_control)
1894 self.layout().setCurrentWidget(self._page_control)
1895 elif html:
1895 elif html:
1896 self._append_html(text)
1896 self._append_html(text)
1897 else:
1897 else:
1898 self._append_plain_text(text)
1898 self._append_plain_text(text)
1899
1899
1900 def _set_paging(self, paging):
1900 def _set_paging(self, paging):
1901 """
1901 """
1902 Change the pager to `paging` style.
1902 Change the pager to `paging` style.
1903
1903
1904 XXX: currently, this is limited to switching between 'hsplit' and
1904 XXX: currently, this is limited to switching between 'hsplit' and
1905 'vsplit'.
1905 'vsplit'.
1906
1906
1907 Parameters:
1907 Parameters:
1908 -----------
1908 -----------
1909 paging : string
1909 paging : string
1910 Either "hsplit", "vsplit", or "inside"
1910 Either "hsplit", "vsplit", or "inside"
1911 """
1911 """
1912 if self._splitter is None:
1912 if self._splitter is None:
1913 raise NotImplementedError("""can only switch if --paging=hsplit or
1913 raise NotImplementedError("""can only switch if --paging=hsplit or
1914 --paging=vsplit is used.""")
1914 --paging=vsplit is used.""")
1915 if paging == 'hsplit':
1915 if paging == 'hsplit':
1916 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1916 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1917 elif paging == 'vsplit':
1917 elif paging == 'vsplit':
1918 self._splitter.setOrientation(QtCore.Qt.Vertical)
1918 self._splitter.setOrientation(QtCore.Qt.Vertical)
1919 elif paging == 'inside':
1919 elif paging == 'inside':
1920 raise NotImplementedError("""switching to 'inside' paging not
1920 raise NotImplementedError("""switching to 'inside' paging not
1921 supported yet.""")
1921 supported yet.""")
1922 else:
1922 else:
1923 raise ValueError("unknown paging method '%s'" % paging)
1923 raise ValueError("unknown paging method '%s'" % paging)
1924 self.paging = paging
1924 self.paging = paging
1925
1925
1926 def _prompt_finished(self):
1926 def _prompt_finished(self):
1927 """ Called immediately after a prompt is finished, i.e. when some input
1927 """ Called immediately after a prompt is finished, i.e. when some input
1928 will be processed and a new prompt displayed.
1928 will be processed and a new prompt displayed.
1929 """
1929 """
1930 self._control.setReadOnly(True)
1930 self._control.setReadOnly(True)
1931 self._prompt_finished_hook()
1931 self._prompt_finished_hook()
1932
1932
1933 def _prompt_started(self):
1933 def _prompt_started(self):
1934 """ Called immediately after a new prompt is displayed.
1934 """ Called immediately after a new prompt is displayed.
1935 """
1935 """
1936 # Temporarily disable the maximum block count to permit undo/redo and
1936 # Temporarily disable the maximum block count to permit undo/redo and
1937 # to ensure that the prompt position does not change due to truncation.
1937 # to ensure that the prompt position does not change due to truncation.
1938 self._control.document().setMaximumBlockCount(0)
1938 self._control.document().setMaximumBlockCount(0)
1939 self._control.setUndoRedoEnabled(True)
1939 self._control.setUndoRedoEnabled(True)
1940
1940
1941 # Work around bug in QPlainTextEdit: input method is not re-enabled
1941 # Work around bug in QPlainTextEdit: input method is not re-enabled
1942 # when read-only is disabled.
1942 # when read-only is disabled.
1943 self._control.setReadOnly(False)
1943 self._control.setReadOnly(False)
1944 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1944 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1945
1945
1946 if not self._reading:
1946 if not self._reading:
1947 self._executing = False
1947 self._executing = False
1948 self._prompt_started_hook()
1948 self._prompt_started_hook()
1949
1949
1950 # If the input buffer has changed while executing, load it.
1950 # If the input buffer has changed while executing, load it.
1951 if self._input_buffer_pending:
1951 if self._input_buffer_pending:
1952 self.input_buffer = self._input_buffer_pending
1952 self.input_buffer = self._input_buffer_pending
1953 self._input_buffer_pending = ''
1953 self._input_buffer_pending = ''
1954
1954
1955 self._control.moveCursor(QtGui.QTextCursor.End)
1955 self._control.moveCursor(QtGui.QTextCursor.End)
1956
1956
1957 def _readline(self, prompt='', callback=None):
1957 def _readline(self, prompt='', callback=None):
1958 """ Reads one line of input from the user.
1958 """ Reads one line of input from the user.
1959
1959
1960 Parameters
1960 Parameters
1961 ----------
1961 ----------
1962 prompt : str, optional
1962 prompt : str, optional
1963 The prompt to print before reading the line.
1963 The prompt to print before reading the line.
1964
1964
1965 callback : callable, optional
1965 callback : callable, optional
1966 A callback to execute with the read line. If not specified, input is
1966 A callback to execute with the read line. If not specified, input is
1967 read *synchronously* and this method does not return until it has
1967 read *synchronously* and this method does not return until it has
1968 been read.
1968 been read.
1969
1969
1970 Returns
1970 Returns
1971 -------
1971 -------
1972 If a callback is specified, returns nothing. Otherwise, returns the
1972 If a callback is specified, returns nothing. Otherwise, returns the
1973 input string with the trailing newline stripped.
1973 input string with the trailing newline stripped.
1974 """
1974 """
1975 if self._reading:
1975 if self._reading:
1976 raise RuntimeError('Cannot read a line. Widget is already reading.')
1976 raise RuntimeError('Cannot read a line. Widget is already reading.')
1977
1977
1978 if not callback and not self.isVisible():
1978 if not callback and not self.isVisible():
1979 # If the user cannot see the widget, this function cannot return.
1979 # If the user cannot see the widget, this function cannot return.
1980 raise RuntimeError('Cannot synchronously read a line if the widget '
1980 raise RuntimeError('Cannot synchronously read a line if the widget '
1981 'is not visible!')
1981 'is not visible!')
1982
1982
1983 self._reading = True
1983 self._reading = True
1984 self._show_prompt(prompt, newline=False)
1984 self._show_prompt(prompt, newline=False)
1985
1985
1986 if callback is None:
1986 if callback is None:
1987 self._reading_callback = None
1987 self._reading_callback = None
1988 while self._reading:
1988 while self._reading:
1989 QtCore.QCoreApplication.processEvents()
1989 QtCore.QCoreApplication.processEvents()
1990 return self._get_input_buffer(force=True).rstrip('\n')
1990 return self._get_input_buffer(force=True).rstrip('\n')
1991
1991
1992 else:
1992 else:
1993 self._reading_callback = lambda: \
1993 self._reading_callback = lambda: \
1994 callback(self._get_input_buffer(force=True).rstrip('\n'))
1994 callback(self._get_input_buffer(force=True).rstrip('\n'))
1995
1995
1996 def _set_continuation_prompt(self, prompt, html=False):
1996 def _set_continuation_prompt(self, prompt, html=False):
1997 """ Sets the continuation prompt.
1997 """ Sets the continuation prompt.
1998
1998
1999 Parameters
1999 Parameters
2000 ----------
2000 ----------
2001 prompt : str
2001 prompt : str
2002 The prompt to show when more input is needed.
2002 The prompt to show when more input is needed.
2003
2003
2004 html : bool, optional (default False)
2004 html : bool, optional (default False)
2005 If set, the prompt will be inserted as formatted HTML. Otherwise,
2005 If set, the prompt will be inserted as formatted HTML. Otherwise,
2006 the prompt will be treated as plain text, though ANSI color codes
2006 the prompt will be treated as plain text, though ANSI color codes
2007 will be handled.
2007 will be handled.
2008 """
2008 """
2009 if html:
2009 if html:
2010 self._continuation_prompt_html = prompt
2010 self._continuation_prompt_html = prompt
2011 else:
2011 else:
2012 self._continuation_prompt = prompt
2012 self._continuation_prompt = prompt
2013 self._continuation_prompt_html = None
2013 self._continuation_prompt_html = None
2014
2014
2015 def _set_cursor(self, cursor):
2015 def _set_cursor(self, cursor):
2016 """ Convenience method to set the current cursor.
2016 """ Convenience method to set the current cursor.
2017 """
2017 """
2018 self._control.setTextCursor(cursor)
2018 self._control.setTextCursor(cursor)
2019
2019
2020 def _set_top_cursor(self, cursor):
2020 def _set_top_cursor(self, cursor):
2021 """ Scrolls the viewport so that the specified cursor is at the top.
2021 """ Scrolls the viewport so that the specified cursor is at the top.
2022 """
2022 """
2023 scrollbar = self._control.verticalScrollBar()
2023 scrollbar = self._control.verticalScrollBar()
2024 scrollbar.setValue(scrollbar.maximum())
2024 scrollbar.setValue(scrollbar.maximum())
2025 original_cursor = self._control.textCursor()
2025 original_cursor = self._control.textCursor()
2026 self._control.setTextCursor(cursor)
2026 self._control.setTextCursor(cursor)
2027 self._control.ensureCursorVisible()
2027 self._control.ensureCursorVisible()
2028 self._control.setTextCursor(original_cursor)
2028 self._control.setTextCursor(original_cursor)
2029
2029
2030 def _show_prompt(self, prompt=None, html=False, newline=True):
2030 def _show_prompt(self, prompt=None, html=False, newline=True):
2031 """ Writes a new prompt at the end of the buffer.
2031 """ Writes a new prompt at the end of the buffer.
2032
2032
2033 Parameters
2033 Parameters
2034 ----------
2034 ----------
2035 prompt : str, optional
2035 prompt : str, optional
2036 The prompt to show. If not specified, the previous prompt is used.
2036 The prompt to show. If not specified, the previous prompt is used.
2037
2037
2038 html : bool, optional (default False)
2038 html : bool, optional (default False)
2039 Only relevant when a prompt is specified. If set, the prompt will
2039 Only relevant when a prompt is specified. If set, the prompt will
2040 be inserted as formatted HTML. Otherwise, the prompt will be treated
2040 be inserted as formatted HTML. Otherwise, the prompt will be treated
2041 as plain text, though ANSI color codes will be handled.
2041 as plain text, though ANSI color codes will be handled.
2042
2042
2043 newline : bool, optional (default True)
2043 newline : bool, optional (default True)
2044 If set, a new line will be written before showing the prompt if
2044 If set, a new line will be written before showing the prompt if
2045 there is not already a newline at the end of the buffer.
2045 there is not already a newline at the end of the buffer.
2046 """
2046 """
2047 # Save the current end position to support _append*(before_prompt=True).
2047 # Save the current end position to support _append*(before_prompt=True).
2048 cursor = self._get_end_cursor()
2048 cursor = self._get_end_cursor()
2049 self._append_before_prompt_pos = cursor.position()
2049 self._append_before_prompt_pos = cursor.position()
2050
2050
2051 # Insert a preliminary newline, if necessary.
2051 # Insert a preliminary newline, if necessary.
2052 if newline and cursor.position() > 0:
2052 if newline and cursor.position() > 0:
2053 cursor.movePosition(QtGui.QTextCursor.Left,
2053 cursor.movePosition(QtGui.QTextCursor.Left,
2054 QtGui.QTextCursor.KeepAnchor)
2054 QtGui.QTextCursor.KeepAnchor)
2055 if cursor.selection().toPlainText() != '\n':
2055 if cursor.selection().toPlainText() != '\n':
2056 self._append_block()
2056 self._append_block()
2057
2057
2058 # Write the prompt.
2058 # Write the prompt.
2059 self._append_plain_text(self._prompt_sep)
2059 self._append_plain_text(self._prompt_sep)
2060 if prompt is None:
2060 if prompt is None:
2061 if self._prompt_html is None:
2061 if self._prompt_html is None:
2062 self._append_plain_text(self._prompt)
2062 self._append_plain_text(self._prompt)
2063 else:
2063 else:
2064 self._append_html(self._prompt_html)
2064 self._append_html(self._prompt_html)
2065 else:
2065 else:
2066 if html:
2066 if html:
2067 self._prompt = self._append_html_fetching_plain_text(prompt)
2067 self._prompt = self._append_html_fetching_plain_text(prompt)
2068 self._prompt_html = prompt
2068 self._prompt_html = prompt
2069 else:
2069 else:
2070 self._append_plain_text(prompt)
2070 self._append_plain_text(prompt)
2071 self._prompt = prompt
2071 self._prompt = prompt
2072 self._prompt_html = None
2072 self._prompt_html = None
2073
2073
2074 self._flush_pending_stream()
2074 self._flush_pending_stream()
2075 self._prompt_pos = self._get_end_cursor().position()
2075 self._prompt_pos = self._get_end_cursor().position()
2076 self._prompt_started()
2076 self._prompt_started()
2077
2077
2078 #------ Signal handlers ----------------------------------------------------
2078 #------ Signal handlers ----------------------------------------------------
2079
2079
2080 def _adjust_scrollbars(self):
2080 def _adjust_scrollbars(self):
2081 """ Expands the vertical scrollbar beyond the range set by Qt.
2081 """ Expands the vertical scrollbar beyond the range set by Qt.
2082 """
2082 """
2083 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2083 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2084 # and qtextedit.cpp.
2084 # and qtextedit.cpp.
2085 document = self._control.document()
2085 document = self._control.document()
2086 scrollbar = self._control.verticalScrollBar()
2086 scrollbar = self._control.verticalScrollBar()
2087 viewport_height = self._control.viewport().height()
2087 viewport_height = self._control.viewport().height()
2088 if isinstance(self._control, QtGui.QPlainTextEdit):
2088 if isinstance(self._control, QtGui.QPlainTextEdit):
2089 maximum = max(0, document.lineCount() - 1)
2089 maximum = max(0, document.lineCount() - 1)
2090 step = viewport_height / self._control.fontMetrics().lineSpacing()
2090 step = viewport_height / self._control.fontMetrics().lineSpacing()
2091 else:
2091 else:
2092 # QTextEdit does not do line-based layout and blocks will not in
2092 # QTextEdit does not do line-based layout and blocks will not in
2093 # general have the same height. Therefore it does not make sense to
2093 # general have the same height. Therefore it does not make sense to
2094 # attempt to scroll in line height increments.
2094 # attempt to scroll in line height increments.
2095 maximum = document.size().height()
2095 maximum = document.size().height()
2096 step = viewport_height
2096 step = viewport_height
2097 diff = maximum - scrollbar.maximum()
2097 diff = maximum - scrollbar.maximum()
2098 scrollbar.setRange(0, maximum)
2098 scrollbar.setRange(0, maximum)
2099 scrollbar.setPageStep(step)
2099 scrollbar.setPageStep(step)
2100
2100
2101 # Compensate for undesirable scrolling that occurs automatically due to
2101 # Compensate for undesirable scrolling that occurs automatically due to
2102 # maximumBlockCount() text truncation.
2102 # maximumBlockCount() text truncation.
2103 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2103 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2104 scrollbar.setValue(scrollbar.value() + diff)
2104 scrollbar.setValue(scrollbar.value() + diff)
2105
2105
2106 def _custom_context_menu_requested(self, pos):
2106 def _custom_context_menu_requested(self, pos):
2107 """ Shows a context menu at the given QPoint (in widget coordinates).
2107 """ Shows a context menu at the given QPoint (in widget coordinates).
2108 """
2108 """
2109 menu = self._context_menu_make(pos)
2109 menu = self._context_menu_make(pos)
2110 menu.exec_(self._control.mapToGlobal(pos))
2110 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,784 +1,784 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import uuid
6 import uuid
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from IPython.external import qt
10 from IPython.external import qt
11 from IPython.external.qt import QtCore, QtGui
11 from IPython.external.qt import QtCore, QtGui
12
12
13 # Local imports
13 # Local imports
14 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
14 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
15 from IPython.core.inputtransformer import classic_prompt
15 from IPython.core.inputtransformer import classic_prompt
16 from IPython.core.oinspect import call_tip
16 from IPython.core.oinspect import call_tip
17 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
17 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
18 from IPython.utils.traitlets import Bool, Instance, Unicode
18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 from bracket_matcher import BracketMatcher
19 from .bracket_matcher import BracketMatcher
20 from call_tip_widget import CallTipWidget
20 from .call_tip_widget import CallTipWidget
21 from completion_lexer import CompletionLexer
21 from .completion_lexer import CompletionLexer
22 from history_console_widget import HistoryConsoleWidget
22 from .history_console_widget import HistoryConsoleWidget
23 from pygments_highlighter import PygmentsHighlighter
23 from .pygments_highlighter import PygmentsHighlighter
24
24
25
25
26 class FrontendHighlighter(PygmentsHighlighter):
26 class FrontendHighlighter(PygmentsHighlighter):
27 """ A PygmentsHighlighter that understands and ignores prompts.
27 """ A PygmentsHighlighter that understands and ignores prompts.
28 """
28 """
29
29
30 def __init__(self, frontend):
30 def __init__(self, frontend):
31 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 self._current_offset = 0
32 self._current_offset = 0
33 self._frontend = frontend
33 self._frontend = frontend
34 self.highlighting_on = False
34 self.highlighting_on = False
35
35
36 def highlightBlock(self, string):
36 def highlightBlock(self, string):
37 """ Highlight a block of text. Reimplemented to highlight selectively.
37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 """
38 """
39 if not self.highlighting_on:
39 if not self.highlighting_on:
40 return
40 return
41
41
42 # The input to this function is a unicode string that may contain
42 # The input to this function is a unicode string that may contain
43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 # the string as plain text so we can compare it.
44 # the string as plain text so we can compare it.
45 current_block = self.currentBlock()
45 current_block = self.currentBlock()
46 string = self._frontend._get_block_plain_text(current_block)
46 string = self._frontend._get_block_plain_text(current_block)
47
47
48 # Decide whether to check for the regular or continuation prompt.
48 # Decide whether to check for the regular or continuation prompt.
49 if current_block.contains(self._frontend._prompt_pos):
49 if current_block.contains(self._frontend._prompt_pos):
50 prompt = self._frontend._prompt
50 prompt = self._frontend._prompt
51 else:
51 else:
52 prompt = self._frontend._continuation_prompt
52 prompt = self._frontend._continuation_prompt
53
53
54 # Only highlight if we can identify a prompt, but make sure not to
54 # Only highlight if we can identify a prompt, but make sure not to
55 # highlight the prompt.
55 # highlight the prompt.
56 if string.startswith(prompt):
56 if string.startswith(prompt):
57 self._current_offset = len(prompt)
57 self._current_offset = len(prompt)
58 string = string[len(prompt):]
58 string = string[len(prompt):]
59 super(FrontendHighlighter, self).highlightBlock(string)
59 super(FrontendHighlighter, self).highlightBlock(string)
60
60
61 def rehighlightBlock(self, block):
61 def rehighlightBlock(self, block):
62 """ Reimplemented to temporarily enable highlighting if disabled.
62 """ Reimplemented to temporarily enable highlighting if disabled.
63 """
63 """
64 old = self.highlighting_on
64 old = self.highlighting_on
65 self.highlighting_on = True
65 self.highlighting_on = True
66 super(FrontendHighlighter, self).rehighlightBlock(block)
66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 self.highlighting_on = old
67 self.highlighting_on = old
68
68
69 def setFormat(self, start, count, format):
69 def setFormat(self, start, count, format):
70 """ Reimplemented to highlight selectively.
70 """ Reimplemented to highlight selectively.
71 """
71 """
72 start += self._current_offset
72 start += self._current_offset
73 super(FrontendHighlighter, self).setFormat(start, count, format)
73 super(FrontendHighlighter, self).setFormat(start, count, format)
74
74
75
75
76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 """ A Qt frontend for a generic Python kernel.
77 """ A Qt frontend for a generic Python kernel.
78 """
78 """
79
79
80 # The text to show when the kernel is (re)started.
80 # The text to show when the kernel is (re)started.
81 banner = Unicode(config=True)
81 banner = Unicode(config=True)
82
82
83 # An option and corresponding signal for overriding the default kernel
83 # An option and corresponding signal for overriding the default kernel
84 # interrupt behavior.
84 # interrupt behavior.
85 custom_interrupt = Bool(False)
85 custom_interrupt = Bool(False)
86 custom_interrupt_requested = QtCore.Signal()
86 custom_interrupt_requested = QtCore.Signal()
87
87
88 # An option and corresponding signals for overriding the default kernel
88 # An option and corresponding signals for overriding the default kernel
89 # restart behavior.
89 # restart behavior.
90 custom_restart = Bool(False)
90 custom_restart = Bool(False)
91 custom_restart_kernel_died = QtCore.Signal(float)
91 custom_restart_kernel_died = QtCore.Signal(float)
92 custom_restart_requested = QtCore.Signal()
92 custom_restart_requested = QtCore.Signal()
93
93
94 # Whether to automatically show calltips on open-parentheses.
94 # Whether to automatically show calltips on open-parentheses.
95 enable_calltips = Bool(True, config=True,
95 enable_calltips = Bool(True, config=True,
96 help="Whether to draw information calltips on open-parentheses.")
96 help="Whether to draw information calltips on open-parentheses.")
97
97
98 clear_on_kernel_restart = Bool(True, config=True,
98 clear_on_kernel_restart = Bool(True, config=True,
99 help="Whether to clear the console when the kernel is restarted")
99 help="Whether to clear the console when the kernel is restarted")
100
100
101 confirm_restart = Bool(True, config=True,
101 confirm_restart = Bool(True, config=True,
102 help="Whether to ask for user confirmation when restarting kernel")
102 help="Whether to ask for user confirmation when restarting kernel")
103
103
104 # Emitted when a user visible 'execute_request' has been submitted to the
104 # Emitted when a user visible 'execute_request' has been submitted to the
105 # kernel from the FrontendWidget. Contains the code to be executed.
105 # kernel from the FrontendWidget. Contains the code to be executed.
106 executing = QtCore.Signal(object)
106 executing = QtCore.Signal(object)
107
107
108 # Emitted when a user-visible 'execute_reply' has been received from the
108 # Emitted when a user-visible 'execute_reply' has been received from the
109 # kernel and processed by the FrontendWidget. Contains the response message.
109 # kernel and processed by the FrontendWidget. Contains the response message.
110 executed = QtCore.Signal(object)
110 executed = QtCore.Signal(object)
111
111
112 # Emitted when an exit request has been received from the kernel.
112 # Emitted when an exit request has been received from the kernel.
113 exit_requested = QtCore.Signal(object)
113 exit_requested = QtCore.Signal(object)
114
114
115 # Protected class variables.
115 # Protected class variables.
116 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
116 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
117 logical_line_transforms=[],
117 logical_line_transforms=[],
118 python_line_transforms=[],
118 python_line_transforms=[],
119 )
119 )
120 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
120 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
121 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
121 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
122 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
122 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
123 _input_splitter_class = InputSplitter
123 _input_splitter_class = InputSplitter
124 _local_kernel = False
124 _local_kernel = False
125 _highlighter = Instance(FrontendHighlighter)
125 _highlighter = Instance(FrontendHighlighter)
126
126
127 #---------------------------------------------------------------------------
127 #---------------------------------------------------------------------------
128 # 'object' interface
128 # 'object' interface
129 #---------------------------------------------------------------------------
129 #---------------------------------------------------------------------------
130
130
131 def __init__(self, *args, **kw):
131 def __init__(self, *args, **kw):
132 super(FrontendWidget, self).__init__(*args, **kw)
132 super(FrontendWidget, self).__init__(*args, **kw)
133 # FIXME: remove this when PySide min version is updated past 1.0.7
133 # FIXME: remove this when PySide min version is updated past 1.0.7
134 # forcefully disable calltips if PySide is < 1.0.7, because they crash
134 # forcefully disable calltips if PySide is < 1.0.7, because they crash
135 if qt.QT_API == qt.QT_API_PYSIDE:
135 if qt.QT_API == qt.QT_API_PYSIDE:
136 import PySide
136 import PySide
137 if PySide.__version_info__ < (1,0,7):
137 if PySide.__version_info__ < (1,0,7):
138 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
138 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
139 self.enable_calltips = False
139 self.enable_calltips = False
140
140
141 # FrontendWidget protected variables.
141 # FrontendWidget protected variables.
142 self._bracket_matcher = BracketMatcher(self._control)
142 self._bracket_matcher = BracketMatcher(self._control)
143 self._call_tip_widget = CallTipWidget(self._control)
143 self._call_tip_widget = CallTipWidget(self._control)
144 self._completion_lexer = CompletionLexer(PythonLexer())
144 self._completion_lexer = CompletionLexer(PythonLexer())
145 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
145 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
146 self._hidden = False
146 self._hidden = False
147 self._highlighter = FrontendHighlighter(self)
147 self._highlighter = FrontendHighlighter(self)
148 self._input_splitter = self._input_splitter_class()
148 self._input_splitter = self._input_splitter_class()
149 self._kernel_manager = None
149 self._kernel_manager = None
150 self._kernel_client = None
150 self._kernel_client = None
151 self._request_info = {}
151 self._request_info = {}
152 self._request_info['execute'] = {};
152 self._request_info['execute'] = {};
153 self._callback_dict = {}
153 self._callback_dict = {}
154
154
155 # Configure the ConsoleWidget.
155 # Configure the ConsoleWidget.
156 self.tab_width = 4
156 self.tab_width = 4
157 self._set_continuation_prompt('... ')
157 self._set_continuation_prompt('... ')
158
158
159 # Configure the CallTipWidget.
159 # Configure the CallTipWidget.
160 self._call_tip_widget.setFont(self.font)
160 self._call_tip_widget.setFont(self.font)
161 self.font_changed.connect(self._call_tip_widget.setFont)
161 self.font_changed.connect(self._call_tip_widget.setFont)
162
162
163 # Configure actions.
163 # Configure actions.
164 action = self._copy_raw_action
164 action = self._copy_raw_action
165 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
165 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
166 action.setEnabled(False)
166 action.setEnabled(False)
167 action.setShortcut(QtGui.QKeySequence(key))
167 action.setShortcut(QtGui.QKeySequence(key))
168 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
168 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
169 action.triggered.connect(self.copy_raw)
169 action.triggered.connect(self.copy_raw)
170 self.copy_available.connect(action.setEnabled)
170 self.copy_available.connect(action.setEnabled)
171 self.addAction(action)
171 self.addAction(action)
172
172
173 # Connect signal handlers.
173 # Connect signal handlers.
174 document = self._control.document()
174 document = self._control.document()
175 document.contentsChange.connect(self._document_contents_change)
175 document.contentsChange.connect(self._document_contents_change)
176
176
177 # Set flag for whether we are connected via localhost.
177 # Set flag for whether we are connected via localhost.
178 self._local_kernel = kw.get('local_kernel',
178 self._local_kernel = kw.get('local_kernel',
179 FrontendWidget._local_kernel)
179 FrontendWidget._local_kernel)
180
180
181 #---------------------------------------------------------------------------
181 #---------------------------------------------------------------------------
182 # 'ConsoleWidget' public interface
182 # 'ConsoleWidget' public interface
183 #---------------------------------------------------------------------------
183 #---------------------------------------------------------------------------
184
184
185 def copy(self):
185 def copy(self):
186 """ Copy the currently selected text to the clipboard, removing prompts.
186 """ Copy the currently selected text to the clipboard, removing prompts.
187 """
187 """
188 if self._page_control is not None and self._page_control.hasFocus():
188 if self._page_control is not None and self._page_control.hasFocus():
189 self._page_control.copy()
189 self._page_control.copy()
190 elif self._control.hasFocus():
190 elif self._control.hasFocus():
191 text = self._control.textCursor().selection().toPlainText()
191 text = self._control.textCursor().selection().toPlainText()
192 if text:
192 if text:
193 text = self._prompt_transformer.transform_cell(text)
193 text = self._prompt_transformer.transform_cell(text)
194 QtGui.QApplication.clipboard().setText(text)
194 QtGui.QApplication.clipboard().setText(text)
195 else:
195 else:
196 self.log.debug("frontend widget : unknown copy target")
196 self.log.debug("frontend widget : unknown copy target")
197
197
198 #---------------------------------------------------------------------------
198 #---------------------------------------------------------------------------
199 # 'ConsoleWidget' abstract interface
199 # 'ConsoleWidget' abstract interface
200 #---------------------------------------------------------------------------
200 #---------------------------------------------------------------------------
201
201
202 def _is_complete(self, source, interactive):
202 def _is_complete(self, source, interactive):
203 """ Returns whether 'source' can be completely processed and a new
203 """ Returns whether 'source' can be completely processed and a new
204 prompt created. When triggered by an Enter/Return key press,
204 prompt created. When triggered by an Enter/Return key press,
205 'interactive' is True; otherwise, it is False.
205 'interactive' is True; otherwise, it is False.
206 """
206 """
207 self._input_splitter.reset()
207 self._input_splitter.reset()
208 complete = self._input_splitter.push(source)
208 complete = self._input_splitter.push(source)
209 if interactive:
209 if interactive:
210 complete = not self._input_splitter.push_accepts_more()
210 complete = not self._input_splitter.push_accepts_more()
211 return complete
211 return complete
212
212
213 def _execute(self, source, hidden):
213 def _execute(self, source, hidden):
214 """ Execute 'source'. If 'hidden', do not show any output.
214 """ Execute 'source'. If 'hidden', do not show any output.
215
215
216 See parent class :meth:`execute` docstring for full details.
216 See parent class :meth:`execute` docstring for full details.
217 """
217 """
218 msg_id = self.kernel_client.execute(source, hidden)
218 msg_id = self.kernel_client.execute(source, hidden)
219 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
219 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
220 self._hidden = hidden
220 self._hidden = hidden
221 if not hidden:
221 if not hidden:
222 self.executing.emit(source)
222 self.executing.emit(source)
223
223
224 def _prompt_started_hook(self):
224 def _prompt_started_hook(self):
225 """ Called immediately after a new prompt is displayed.
225 """ Called immediately after a new prompt is displayed.
226 """
226 """
227 if not self._reading:
227 if not self._reading:
228 self._highlighter.highlighting_on = True
228 self._highlighter.highlighting_on = True
229
229
230 def _prompt_finished_hook(self):
230 def _prompt_finished_hook(self):
231 """ Called immediately after a prompt is finished, i.e. when some input
231 """ Called immediately after a prompt is finished, i.e. when some input
232 will be processed and a new prompt displayed.
232 will be processed and a new prompt displayed.
233 """
233 """
234 # Flush all state from the input splitter so the next round of
234 # Flush all state from the input splitter so the next round of
235 # reading input starts with a clean buffer.
235 # reading input starts with a clean buffer.
236 self._input_splitter.reset()
236 self._input_splitter.reset()
237
237
238 if not self._reading:
238 if not self._reading:
239 self._highlighter.highlighting_on = False
239 self._highlighter.highlighting_on = False
240
240
241 def _tab_pressed(self):
241 def _tab_pressed(self):
242 """ Called when the tab key is pressed. Returns whether to continue
242 """ Called when the tab key is pressed. Returns whether to continue
243 processing the event.
243 processing the event.
244 """
244 """
245 # Perform tab completion if:
245 # Perform tab completion if:
246 # 1) The cursor is in the input buffer.
246 # 1) The cursor is in the input buffer.
247 # 2) There is a non-whitespace character before the cursor.
247 # 2) There is a non-whitespace character before the cursor.
248 text = self._get_input_buffer_cursor_line()
248 text = self._get_input_buffer_cursor_line()
249 if text is None:
249 if text is None:
250 return False
250 return False
251 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
251 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
252 if complete:
252 if complete:
253 self._complete()
253 self._complete()
254 return not complete
254 return not complete
255
255
256 #---------------------------------------------------------------------------
256 #---------------------------------------------------------------------------
257 # 'ConsoleWidget' protected interface
257 # 'ConsoleWidget' protected interface
258 #---------------------------------------------------------------------------
258 #---------------------------------------------------------------------------
259
259
260 def _context_menu_make(self, pos):
260 def _context_menu_make(self, pos):
261 """ Reimplemented to add an action for raw copy.
261 """ Reimplemented to add an action for raw copy.
262 """
262 """
263 menu = super(FrontendWidget, self)._context_menu_make(pos)
263 menu = super(FrontendWidget, self)._context_menu_make(pos)
264 for before_action in menu.actions():
264 for before_action in menu.actions():
265 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
265 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
266 QtGui.QKeySequence.ExactMatch:
266 QtGui.QKeySequence.ExactMatch:
267 menu.insertAction(before_action, self._copy_raw_action)
267 menu.insertAction(before_action, self._copy_raw_action)
268 break
268 break
269 return menu
269 return menu
270
270
271 def request_interrupt_kernel(self):
271 def request_interrupt_kernel(self):
272 if self._executing:
272 if self._executing:
273 self.interrupt_kernel()
273 self.interrupt_kernel()
274
274
275 def request_restart_kernel(self):
275 def request_restart_kernel(self):
276 message = 'Are you sure you want to restart the kernel?'
276 message = 'Are you sure you want to restart the kernel?'
277 self.restart_kernel(message, now=False)
277 self.restart_kernel(message, now=False)
278
278
279 def _event_filter_console_keypress(self, event):
279 def _event_filter_console_keypress(self, event):
280 """ Reimplemented for execution interruption and smart backspace.
280 """ Reimplemented for execution interruption and smart backspace.
281 """
281 """
282 key = event.key()
282 key = event.key()
283 if self._control_key_down(event.modifiers(), include_command=False):
283 if self._control_key_down(event.modifiers(), include_command=False):
284
284
285 if key == QtCore.Qt.Key_C and self._executing:
285 if key == QtCore.Qt.Key_C and self._executing:
286 self.request_interrupt_kernel()
286 self.request_interrupt_kernel()
287 return True
287 return True
288
288
289 elif key == QtCore.Qt.Key_Period:
289 elif key == QtCore.Qt.Key_Period:
290 self.request_restart_kernel()
290 self.request_restart_kernel()
291 return True
291 return True
292
292
293 elif not event.modifiers() & QtCore.Qt.AltModifier:
293 elif not event.modifiers() & QtCore.Qt.AltModifier:
294
294
295 # Smart backspace: remove four characters in one backspace if:
295 # Smart backspace: remove four characters in one backspace if:
296 # 1) everything left of the cursor is whitespace
296 # 1) everything left of the cursor is whitespace
297 # 2) the four characters immediately left of the cursor are spaces
297 # 2) the four characters immediately left of the cursor are spaces
298 if key == QtCore.Qt.Key_Backspace:
298 if key == QtCore.Qt.Key_Backspace:
299 col = self._get_input_buffer_cursor_column()
299 col = self._get_input_buffer_cursor_column()
300 cursor = self._control.textCursor()
300 cursor = self._control.textCursor()
301 if col > 3 and not cursor.hasSelection():
301 if col > 3 and not cursor.hasSelection():
302 text = self._get_input_buffer_cursor_line()[:col]
302 text = self._get_input_buffer_cursor_line()[:col]
303 if text.endswith(' ') and not text.strip():
303 if text.endswith(' ') and not text.strip():
304 cursor.movePosition(QtGui.QTextCursor.Left,
304 cursor.movePosition(QtGui.QTextCursor.Left,
305 QtGui.QTextCursor.KeepAnchor, 4)
305 QtGui.QTextCursor.KeepAnchor, 4)
306 cursor.removeSelectedText()
306 cursor.removeSelectedText()
307 return True
307 return True
308
308
309 return super(FrontendWidget, self)._event_filter_console_keypress(event)
309 return super(FrontendWidget, self)._event_filter_console_keypress(event)
310
310
311 def _insert_continuation_prompt(self, cursor):
311 def _insert_continuation_prompt(self, cursor):
312 """ Reimplemented for auto-indentation.
312 """ Reimplemented for auto-indentation.
313 """
313 """
314 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
314 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
315 cursor.insertText(' ' * self._input_splitter.indent_spaces)
315 cursor.insertText(' ' * self._input_splitter.indent_spaces)
316
316
317 #---------------------------------------------------------------------------
317 #---------------------------------------------------------------------------
318 # 'BaseFrontendMixin' abstract interface
318 # 'BaseFrontendMixin' abstract interface
319 #---------------------------------------------------------------------------
319 #---------------------------------------------------------------------------
320
320
321 def _handle_complete_reply(self, rep):
321 def _handle_complete_reply(self, rep):
322 """ Handle replies for tab completion.
322 """ Handle replies for tab completion.
323 """
323 """
324 self.log.debug("complete: %s", rep.get('content', ''))
324 self.log.debug("complete: %s", rep.get('content', ''))
325 cursor = self._get_cursor()
325 cursor = self._get_cursor()
326 info = self._request_info.get('complete')
326 info = self._request_info.get('complete')
327 if info and info.id == rep['parent_header']['msg_id'] and \
327 if info and info.id == rep['parent_header']['msg_id'] and \
328 info.pos == cursor.position():
328 info.pos == cursor.position():
329 text = '.'.join(self._get_context())
329 text = '.'.join(self._get_context())
330 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
330 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
331 self._complete_with_items(cursor, rep['content']['matches'])
331 self._complete_with_items(cursor, rep['content']['matches'])
332
332
333 def _silent_exec_callback(self, expr, callback):
333 def _silent_exec_callback(self, expr, callback):
334 """Silently execute `expr` in the kernel and call `callback` with reply
334 """Silently execute `expr` in the kernel and call `callback` with reply
335
335
336 the `expr` is evaluated silently in the kernel (without) output in
336 the `expr` is evaluated silently in the kernel (without) output in
337 the frontend. Call `callback` with the
337 the frontend. Call `callback` with the
338 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
338 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
339
339
340 Parameters
340 Parameters
341 ----------
341 ----------
342 expr : string
342 expr : string
343 valid string to be executed by the kernel.
343 valid string to be executed by the kernel.
344 callback : function
344 callback : function
345 function accepting one argument, as a string. The string will be
345 function accepting one argument, as a string. The string will be
346 the `repr` of the result of evaluating `expr`
346 the `repr` of the result of evaluating `expr`
347
347
348 The `callback` is called with the `repr()` of the result of `expr` as
348 The `callback` is called with the `repr()` of the result of `expr` as
349 first argument. To get the object, do `eval()` on the passed value.
349 first argument. To get the object, do `eval()` on the passed value.
350
350
351 See Also
351 See Also
352 --------
352 --------
353 _handle_exec_callback : private method, deal with calling callback with reply
353 _handle_exec_callback : private method, deal with calling callback with reply
354
354
355 """
355 """
356
356
357 # generate uuid, which would be used as an indication of whether or
357 # generate uuid, which would be used as an indication of whether or
358 # not the unique request originated from here (can use msg id ?)
358 # not the unique request originated from here (can use msg id ?)
359 local_uuid = str(uuid.uuid1())
359 local_uuid = str(uuid.uuid1())
360 msg_id = self.kernel_client.execute('',
360 msg_id = self.kernel_client.execute('',
361 silent=True, user_expressions={ local_uuid:expr })
361 silent=True, user_expressions={ local_uuid:expr })
362 self._callback_dict[local_uuid] = callback
362 self._callback_dict[local_uuid] = callback
363 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
363 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
364
364
365 def _handle_exec_callback(self, msg):
365 def _handle_exec_callback(self, msg):
366 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
366 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
367
367
368 Parameters
368 Parameters
369 ----------
369 ----------
370 msg : raw message send by the kernel containing an `user_expressions`
370 msg : raw message send by the kernel containing an `user_expressions`
371 and having a 'silent_exec_callback' kind.
371 and having a 'silent_exec_callback' kind.
372
372
373 Notes
373 Notes
374 -----
374 -----
375 This function will look for a `callback` associated with the
375 This function will look for a `callback` associated with the
376 corresponding message id. Association has been made by
376 corresponding message id. Association has been made by
377 `_silent_exec_callback`. `callback` is then called with the `repr()`
377 `_silent_exec_callback`. `callback` is then called with the `repr()`
378 of the value of corresponding `user_expressions` as argument.
378 of the value of corresponding `user_expressions` as argument.
379 `callback` is then removed from the known list so that any message
379 `callback` is then removed from the known list so that any message
380 coming again with the same id won't trigger it.
380 coming again with the same id won't trigger it.
381
381
382 """
382 """
383
383
384 user_exp = msg['content'].get('user_expressions')
384 user_exp = msg['content'].get('user_expressions')
385 if not user_exp:
385 if not user_exp:
386 return
386 return
387 for expression in user_exp:
387 for expression in user_exp:
388 if expression in self._callback_dict:
388 if expression in self._callback_dict:
389 self._callback_dict.pop(expression)(user_exp[expression])
389 self._callback_dict.pop(expression)(user_exp[expression])
390
390
391 def _handle_execute_reply(self, msg):
391 def _handle_execute_reply(self, msg):
392 """ Handles replies for code execution.
392 """ Handles replies for code execution.
393 """
393 """
394 self.log.debug("execute: %s", msg.get('content', ''))
394 self.log.debug("execute: %s", msg.get('content', ''))
395 msg_id = msg['parent_header']['msg_id']
395 msg_id = msg['parent_header']['msg_id']
396 info = self._request_info['execute'].get(msg_id)
396 info = self._request_info['execute'].get(msg_id)
397 # unset reading flag, because if execute finished, raw_input can't
397 # unset reading flag, because if execute finished, raw_input can't
398 # still be pending.
398 # still be pending.
399 self._reading = False
399 self._reading = False
400 if info and info.kind == 'user' and not self._hidden:
400 if info and info.kind == 'user' and not self._hidden:
401 # Make sure that all output from the SUB channel has been processed
401 # Make sure that all output from the SUB channel has been processed
402 # before writing a new prompt.
402 # before writing a new prompt.
403 self.kernel_client.iopub_channel.flush()
403 self.kernel_client.iopub_channel.flush()
404
404
405 # Reset the ANSI style information to prevent bad text in stdout
405 # Reset the ANSI style information to prevent bad text in stdout
406 # from messing up our colors. We're not a true terminal so we're
406 # from messing up our colors. We're not a true terminal so we're
407 # allowed to do this.
407 # allowed to do this.
408 if self.ansi_codes:
408 if self.ansi_codes:
409 self._ansi_processor.reset_sgr()
409 self._ansi_processor.reset_sgr()
410
410
411 content = msg['content']
411 content = msg['content']
412 status = content['status']
412 status = content['status']
413 if status == 'ok':
413 if status == 'ok':
414 self._process_execute_ok(msg)
414 self._process_execute_ok(msg)
415 elif status == 'error':
415 elif status == 'error':
416 self._process_execute_error(msg)
416 self._process_execute_error(msg)
417 elif status == 'aborted':
417 elif status == 'aborted':
418 self._process_execute_abort(msg)
418 self._process_execute_abort(msg)
419
419
420 self._show_interpreter_prompt_for_reply(msg)
420 self._show_interpreter_prompt_for_reply(msg)
421 self.executed.emit(msg)
421 self.executed.emit(msg)
422 self._request_info['execute'].pop(msg_id)
422 self._request_info['execute'].pop(msg_id)
423 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
423 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
424 self._handle_exec_callback(msg)
424 self._handle_exec_callback(msg)
425 self._request_info['execute'].pop(msg_id)
425 self._request_info['execute'].pop(msg_id)
426 else:
426 else:
427 super(FrontendWidget, self)._handle_execute_reply(msg)
427 super(FrontendWidget, self)._handle_execute_reply(msg)
428
428
429 def _handle_input_request(self, msg):
429 def _handle_input_request(self, msg):
430 """ Handle requests for raw_input.
430 """ Handle requests for raw_input.
431 """
431 """
432 self.log.debug("input: %s", msg.get('content', ''))
432 self.log.debug("input: %s", msg.get('content', ''))
433 if self._hidden:
433 if self._hidden:
434 raise RuntimeError('Request for raw input during hidden execution.')
434 raise RuntimeError('Request for raw input during hidden execution.')
435
435
436 # Make sure that all output from the SUB channel has been processed
436 # Make sure that all output from the SUB channel has been processed
437 # before entering readline mode.
437 # before entering readline mode.
438 self.kernel_client.iopub_channel.flush()
438 self.kernel_client.iopub_channel.flush()
439
439
440 def callback(line):
440 def callback(line):
441 self.kernel_client.stdin_channel.input(line)
441 self.kernel_client.stdin_channel.input(line)
442 if self._reading:
442 if self._reading:
443 self.log.debug("Got second input request, assuming first was interrupted.")
443 self.log.debug("Got second input request, assuming first was interrupted.")
444 self._reading = False
444 self._reading = False
445 self._readline(msg['content']['prompt'], callback=callback)
445 self._readline(msg['content']['prompt'], callback=callback)
446
446
447 def _kernel_restarted_message(self, died=True):
447 def _kernel_restarted_message(self, died=True):
448 msg = "Kernel died, restarting" if died else "Kernel restarting"
448 msg = "Kernel died, restarting" if died else "Kernel restarting"
449 self._append_html("<br>%s<hr><br>" % msg,
449 self._append_html("<br>%s<hr><br>" % msg,
450 before_prompt=False
450 before_prompt=False
451 )
451 )
452
452
453 def _handle_kernel_died(self, since_last_heartbeat):
453 def _handle_kernel_died(self, since_last_heartbeat):
454 """Handle the kernel's death (if we do not own the kernel).
454 """Handle the kernel's death (if we do not own the kernel).
455 """
455 """
456 self.log.warn("kernel died: %s", since_last_heartbeat)
456 self.log.warn("kernel died: %s", since_last_heartbeat)
457 if self.custom_restart:
457 if self.custom_restart:
458 self.custom_restart_kernel_died.emit(since_last_heartbeat)
458 self.custom_restart_kernel_died.emit(since_last_heartbeat)
459 else:
459 else:
460 self._kernel_restarted_message(died=True)
460 self._kernel_restarted_message(died=True)
461 self.reset()
461 self.reset()
462
462
463 def _handle_kernel_restarted(self, died=True):
463 def _handle_kernel_restarted(self, died=True):
464 """Notice that the autorestarter restarted the kernel.
464 """Notice that the autorestarter restarted the kernel.
465
465
466 There's nothing to do but show a message.
466 There's nothing to do but show a message.
467 """
467 """
468 self.log.warn("kernel restarted")
468 self.log.warn("kernel restarted")
469 self._kernel_restarted_message(died=died)
469 self._kernel_restarted_message(died=died)
470 self.reset()
470 self.reset()
471
471
472 def _handle_object_info_reply(self, rep):
472 def _handle_object_info_reply(self, rep):
473 """ Handle replies for call tips.
473 """ Handle replies for call tips.
474 """
474 """
475 self.log.debug("oinfo: %s", rep.get('content', ''))
475 self.log.debug("oinfo: %s", rep.get('content', ''))
476 cursor = self._get_cursor()
476 cursor = self._get_cursor()
477 info = self._request_info.get('call_tip')
477 info = self._request_info.get('call_tip')
478 if info and info.id == rep['parent_header']['msg_id'] and \
478 if info and info.id == rep['parent_header']['msg_id'] and \
479 info.pos == cursor.position():
479 info.pos == cursor.position():
480 # Get the information for a call tip. For now we format the call
480 # Get the information for a call tip. For now we format the call
481 # line as string, later we can pass False to format_call and
481 # line as string, later we can pass False to format_call and
482 # syntax-highlight it ourselves for nicer formatting in the
482 # syntax-highlight it ourselves for nicer formatting in the
483 # calltip.
483 # calltip.
484 content = rep['content']
484 content = rep['content']
485 # if this is from pykernel, 'docstring' will be the only key
485 # if this is from pykernel, 'docstring' will be the only key
486 if content.get('ismagic', False):
486 if content.get('ismagic', False):
487 # Don't generate a call-tip for magics. Ideally, we should
487 # Don't generate a call-tip for magics. Ideally, we should
488 # generate a tooltip, but not on ( like we do for actual
488 # generate a tooltip, but not on ( like we do for actual
489 # callables.
489 # callables.
490 call_info, doc = None, None
490 call_info, doc = None, None
491 else:
491 else:
492 call_info, doc = call_tip(content, format_call=True)
492 call_info, doc = call_tip(content, format_call=True)
493 if call_info or doc:
493 if call_info or doc:
494 self._call_tip_widget.show_call_info(call_info, doc)
494 self._call_tip_widget.show_call_info(call_info, doc)
495
495
496 def _handle_pyout(self, msg):
496 def _handle_pyout(self, msg):
497 """ Handle display hook output.
497 """ Handle display hook output.
498 """
498 """
499 self.log.debug("pyout: %s", msg.get('content', ''))
499 self.log.debug("pyout: %s", msg.get('content', ''))
500 if not self._hidden and self._is_from_this_session(msg):
500 if not self._hidden and self._is_from_this_session(msg):
501 text = msg['content']['data']
501 text = msg['content']['data']
502 self._append_plain_text(text + '\n', before_prompt=True)
502 self._append_plain_text(text + '\n', before_prompt=True)
503
503
504 def _handle_stream(self, msg):
504 def _handle_stream(self, msg):
505 """ Handle stdout, stderr, and stdin.
505 """ Handle stdout, stderr, and stdin.
506 """
506 """
507 self.log.debug("stream: %s", msg.get('content', ''))
507 self.log.debug("stream: %s", msg.get('content', ''))
508 if not self._hidden and self._is_from_this_session(msg):
508 if not self._hidden and self._is_from_this_session(msg):
509 # Most consoles treat tabs as being 8 space characters. Convert tabs
509 # Most consoles treat tabs as being 8 space characters. Convert tabs
510 # to spaces so that output looks as expected regardless of this
510 # to spaces so that output looks as expected regardless of this
511 # widget's tab width.
511 # widget's tab width.
512 text = msg['content']['data'].expandtabs(8)
512 text = msg['content']['data'].expandtabs(8)
513
513
514 self._append_plain_text(text, before_prompt=True)
514 self._append_plain_text(text, before_prompt=True)
515 self._control.moveCursor(QtGui.QTextCursor.End)
515 self._control.moveCursor(QtGui.QTextCursor.End)
516
516
517 def _handle_shutdown_reply(self, msg):
517 def _handle_shutdown_reply(self, msg):
518 """ Handle shutdown signal, only if from other console.
518 """ Handle shutdown signal, only if from other console.
519 """
519 """
520 self.log.warn("shutdown: %s", msg.get('content', ''))
520 self.log.warn("shutdown: %s", msg.get('content', ''))
521 restart = msg.get('content', {}).get('restart', False)
521 restart = msg.get('content', {}).get('restart', False)
522 if not self._hidden and not self._is_from_this_session(msg):
522 if not self._hidden and not self._is_from_this_session(msg):
523 # got shutdown reply, request came from session other than ours
523 # got shutdown reply, request came from session other than ours
524 if restart:
524 if restart:
525 # someone restarted the kernel, handle it
525 # someone restarted the kernel, handle it
526 self._handle_kernel_restarted(died=False)
526 self._handle_kernel_restarted(died=False)
527 else:
527 else:
528 # kernel was shutdown permanently
528 # kernel was shutdown permanently
529 # this triggers exit_requested if the kernel was local,
529 # this triggers exit_requested if the kernel was local,
530 # and a dialog if the kernel was remote,
530 # and a dialog if the kernel was remote,
531 # so we don't suddenly clear the qtconsole without asking.
531 # so we don't suddenly clear the qtconsole without asking.
532 if self._local_kernel:
532 if self._local_kernel:
533 self.exit_requested.emit(self)
533 self.exit_requested.emit(self)
534 else:
534 else:
535 title = self.window().windowTitle()
535 title = self.window().windowTitle()
536 reply = QtGui.QMessageBox.question(self, title,
536 reply = QtGui.QMessageBox.question(self, title,
537 "Kernel has been shutdown permanently. "
537 "Kernel has been shutdown permanently. "
538 "Close the Console?",
538 "Close the Console?",
539 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
539 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
540 if reply == QtGui.QMessageBox.Yes:
540 if reply == QtGui.QMessageBox.Yes:
541 self.exit_requested.emit(self)
541 self.exit_requested.emit(self)
542
542
543 def _handle_status(self, msg):
543 def _handle_status(self, msg):
544 """Handle status message"""
544 """Handle status message"""
545 # This is where a busy/idle indicator would be triggered,
545 # This is where a busy/idle indicator would be triggered,
546 # when we make one.
546 # when we make one.
547 state = msg['content'].get('execution_state', '')
547 state = msg['content'].get('execution_state', '')
548 if state == 'starting':
548 if state == 'starting':
549 # kernel started while we were running
549 # kernel started while we were running
550 if self._executing:
550 if self._executing:
551 self._handle_kernel_restarted(died=True)
551 self._handle_kernel_restarted(died=True)
552 elif state == 'idle':
552 elif state == 'idle':
553 pass
553 pass
554 elif state == 'busy':
554 elif state == 'busy':
555 pass
555 pass
556
556
557 def _started_channels(self):
557 def _started_channels(self):
558 """ Called when the KernelManager channels have started listening or
558 """ Called when the KernelManager channels have started listening or
559 when the frontend is assigned an already listening KernelManager.
559 when the frontend is assigned an already listening KernelManager.
560 """
560 """
561 self.reset(clear=True)
561 self.reset(clear=True)
562
562
563 #---------------------------------------------------------------------------
563 #---------------------------------------------------------------------------
564 # 'FrontendWidget' public interface
564 # 'FrontendWidget' public interface
565 #---------------------------------------------------------------------------
565 #---------------------------------------------------------------------------
566
566
567 def copy_raw(self):
567 def copy_raw(self):
568 """ Copy the currently selected text to the clipboard without attempting
568 """ Copy the currently selected text to the clipboard without attempting
569 to remove prompts or otherwise alter the text.
569 to remove prompts or otherwise alter the text.
570 """
570 """
571 self._control.copy()
571 self._control.copy()
572
572
573 def execute_file(self, path, hidden=False):
573 def execute_file(self, path, hidden=False):
574 """ Attempts to execute file with 'path'. If 'hidden', no output is
574 """ Attempts to execute file with 'path'. If 'hidden', no output is
575 shown.
575 shown.
576 """
576 """
577 self.execute('execfile(%r)' % path, hidden=hidden)
577 self.execute('execfile(%r)' % path, hidden=hidden)
578
578
579 def interrupt_kernel(self):
579 def interrupt_kernel(self):
580 """ Attempts to interrupt the running kernel.
580 """ Attempts to interrupt the running kernel.
581
581
582 Also unsets _reading flag, to avoid runtime errors
582 Also unsets _reading flag, to avoid runtime errors
583 if raw_input is called again.
583 if raw_input is called again.
584 """
584 """
585 if self.custom_interrupt:
585 if self.custom_interrupt:
586 self._reading = False
586 self._reading = False
587 self.custom_interrupt_requested.emit()
587 self.custom_interrupt_requested.emit()
588 elif self.kernel_manager:
588 elif self.kernel_manager:
589 self._reading = False
589 self._reading = False
590 self.kernel_manager.interrupt_kernel()
590 self.kernel_manager.interrupt_kernel()
591 else:
591 else:
592 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
592 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
593
593
594 def reset(self, clear=False):
594 def reset(self, clear=False):
595 """ Resets the widget to its initial state if ``clear`` parameter
595 """ Resets the widget to its initial state if ``clear`` parameter
596 is True, otherwise
596 is True, otherwise
597 prints a visual indication of the fact that the kernel restarted, but
597 prints a visual indication of the fact that the kernel restarted, but
598 does not clear the traces from previous usage of the kernel before it
598 does not clear the traces from previous usage of the kernel before it
599 was restarted. With ``clear=True``, it is similar to ``%clear``, but
599 was restarted. With ``clear=True``, it is similar to ``%clear``, but
600 also re-writes the banner and aborts execution if necessary.
600 also re-writes the banner and aborts execution if necessary.
601 """
601 """
602 if self._executing:
602 if self._executing:
603 self._executing = False
603 self._executing = False
604 self._request_info['execute'] = {}
604 self._request_info['execute'] = {}
605 self._reading = False
605 self._reading = False
606 self._highlighter.highlighting_on = False
606 self._highlighter.highlighting_on = False
607
607
608 if clear:
608 if clear:
609 self._control.clear()
609 self._control.clear()
610 self._append_plain_text(self.banner)
610 self._append_plain_text(self.banner)
611 # update output marker for stdout/stderr, so that startup
611 # update output marker for stdout/stderr, so that startup
612 # messages appear after banner:
612 # messages appear after banner:
613 self._append_before_prompt_pos = self._get_cursor().position()
613 self._append_before_prompt_pos = self._get_cursor().position()
614 self._show_interpreter_prompt()
614 self._show_interpreter_prompt()
615
615
616 def restart_kernel(self, message, now=False):
616 def restart_kernel(self, message, now=False):
617 """ Attempts to restart the running kernel.
617 """ Attempts to restart the running kernel.
618 """
618 """
619 # FIXME: now should be configurable via a checkbox in the dialog. Right
619 # FIXME: now should be configurable via a checkbox in the dialog. Right
620 # now at least the heartbeat path sets it to True and the manual restart
620 # now at least the heartbeat path sets it to True and the manual restart
621 # to False. But those should just be the pre-selected states of a
621 # to False. But those should just be the pre-selected states of a
622 # checkbox that the user could override if so desired. But I don't know
622 # checkbox that the user could override if so desired. But I don't know
623 # enough Qt to go implementing the checkbox now.
623 # enough Qt to go implementing the checkbox now.
624
624
625 if self.custom_restart:
625 if self.custom_restart:
626 self.custom_restart_requested.emit()
626 self.custom_restart_requested.emit()
627 return
627 return
628
628
629 if self.kernel_manager:
629 if self.kernel_manager:
630 # Pause the heart beat channel to prevent further warnings.
630 # Pause the heart beat channel to prevent further warnings.
631 self.kernel_client.hb_channel.pause()
631 self.kernel_client.hb_channel.pause()
632
632
633 # Prompt the user to restart the kernel. Un-pause the heartbeat if
633 # Prompt the user to restart the kernel. Un-pause the heartbeat if
634 # they decline. (If they accept, the heartbeat will be un-paused
634 # they decline. (If they accept, the heartbeat will be un-paused
635 # automatically when the kernel is restarted.)
635 # automatically when the kernel is restarted.)
636 if self.confirm_restart:
636 if self.confirm_restart:
637 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
637 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
638 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
638 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
639 message, buttons)
639 message, buttons)
640 do_restart = result == QtGui.QMessageBox.Yes
640 do_restart = result == QtGui.QMessageBox.Yes
641 else:
641 else:
642 # confirm_restart is False, so we don't need to ask user
642 # confirm_restart is False, so we don't need to ask user
643 # anything, just do the restart
643 # anything, just do the restart
644 do_restart = True
644 do_restart = True
645 if do_restart:
645 if do_restart:
646 try:
646 try:
647 self.kernel_manager.restart_kernel(now=now)
647 self.kernel_manager.restart_kernel(now=now)
648 except RuntimeError as e:
648 except RuntimeError as e:
649 self._append_plain_text(
649 self._append_plain_text(
650 'Error restarting kernel: %s\n' % e,
650 'Error restarting kernel: %s\n' % e,
651 before_prompt=True
651 before_prompt=True
652 )
652 )
653 else:
653 else:
654 self._append_html("<br>Restarting kernel...\n<hr><br>",
654 self._append_html("<br>Restarting kernel...\n<hr><br>",
655 before_prompt=True,
655 before_prompt=True,
656 )
656 )
657 else:
657 else:
658 self.kernel_client.hb_channel.unpause()
658 self.kernel_client.hb_channel.unpause()
659
659
660 else:
660 else:
661 self._append_plain_text(
661 self._append_plain_text(
662 'Cannot restart a Kernel I did not start\n',
662 'Cannot restart a Kernel I did not start\n',
663 before_prompt=True
663 before_prompt=True
664 )
664 )
665
665
666 #---------------------------------------------------------------------------
666 #---------------------------------------------------------------------------
667 # 'FrontendWidget' protected interface
667 # 'FrontendWidget' protected interface
668 #---------------------------------------------------------------------------
668 #---------------------------------------------------------------------------
669
669
670 def _call_tip(self):
670 def _call_tip(self):
671 """ Shows a call tip, if appropriate, at the current cursor location.
671 """ Shows a call tip, if appropriate, at the current cursor location.
672 """
672 """
673 # Decide if it makes sense to show a call tip
673 # Decide if it makes sense to show a call tip
674 if not self.enable_calltips:
674 if not self.enable_calltips:
675 return False
675 return False
676 cursor = self._get_cursor()
676 cursor = self._get_cursor()
677 cursor.movePosition(QtGui.QTextCursor.Left)
677 cursor.movePosition(QtGui.QTextCursor.Left)
678 if cursor.document().characterAt(cursor.position()) != '(':
678 if cursor.document().characterAt(cursor.position()) != '(':
679 return False
679 return False
680 context = self._get_context(cursor)
680 context = self._get_context(cursor)
681 if not context:
681 if not context:
682 return False
682 return False
683
683
684 # Send the metadata request to the kernel
684 # Send the metadata request to the kernel
685 name = '.'.join(context)
685 name = '.'.join(context)
686 msg_id = self.kernel_client.object_info(name)
686 msg_id = self.kernel_client.object_info(name)
687 pos = self._get_cursor().position()
687 pos = self._get_cursor().position()
688 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
688 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
689 return True
689 return True
690
690
691 def _complete(self):
691 def _complete(self):
692 """ Performs completion at the current cursor location.
692 """ Performs completion at the current cursor location.
693 """
693 """
694 context = self._get_context()
694 context = self._get_context()
695 if context:
695 if context:
696 # Send the completion request to the kernel
696 # Send the completion request to the kernel
697 msg_id = self.kernel_client.complete(
697 msg_id = self.kernel_client.complete(
698 '.'.join(context), # text
698 '.'.join(context), # text
699 self._get_input_buffer_cursor_line(), # line
699 self._get_input_buffer_cursor_line(), # line
700 self._get_input_buffer_cursor_column(), # cursor_pos
700 self._get_input_buffer_cursor_column(), # cursor_pos
701 self.input_buffer) # block
701 self.input_buffer) # block
702 pos = self._get_cursor().position()
702 pos = self._get_cursor().position()
703 info = self._CompletionRequest(msg_id, pos)
703 info = self._CompletionRequest(msg_id, pos)
704 self._request_info['complete'] = info
704 self._request_info['complete'] = info
705
705
706 def _get_context(self, cursor=None):
706 def _get_context(self, cursor=None):
707 """ Gets the context for the specified cursor (or the current cursor
707 """ Gets the context for the specified cursor (or the current cursor
708 if none is specified).
708 if none is specified).
709 """
709 """
710 if cursor is None:
710 if cursor is None:
711 cursor = self._get_cursor()
711 cursor = self._get_cursor()
712 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
712 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
713 QtGui.QTextCursor.KeepAnchor)
713 QtGui.QTextCursor.KeepAnchor)
714 text = cursor.selection().toPlainText()
714 text = cursor.selection().toPlainText()
715 return self._completion_lexer.get_context(text)
715 return self._completion_lexer.get_context(text)
716
716
717 def _process_execute_abort(self, msg):
717 def _process_execute_abort(self, msg):
718 """ Process a reply for an aborted execution request.
718 """ Process a reply for an aborted execution request.
719 """
719 """
720 self._append_plain_text("ERROR: execution aborted\n")
720 self._append_plain_text("ERROR: execution aborted\n")
721
721
722 def _process_execute_error(self, msg):
722 def _process_execute_error(self, msg):
723 """ Process a reply for an execution request that resulted in an error.
723 """ Process a reply for an execution request that resulted in an error.
724 """
724 """
725 content = msg['content']
725 content = msg['content']
726 # If a SystemExit is passed along, this means exit() was called - also
726 # If a SystemExit is passed along, this means exit() was called - also
727 # all the ipython %exit magic syntax of '-k' to be used to keep
727 # all the ipython %exit magic syntax of '-k' to be used to keep
728 # the kernel running
728 # the kernel running
729 if content['ename']=='SystemExit':
729 if content['ename']=='SystemExit':
730 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
730 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
731 self._keep_kernel_on_exit = keepkernel
731 self._keep_kernel_on_exit = keepkernel
732 self.exit_requested.emit(self)
732 self.exit_requested.emit(self)
733 else:
733 else:
734 traceback = ''.join(content['traceback'])
734 traceback = ''.join(content['traceback'])
735 self._append_plain_text(traceback)
735 self._append_plain_text(traceback)
736
736
737 def _process_execute_ok(self, msg):
737 def _process_execute_ok(self, msg):
738 """ Process a reply for a successful execution request.
738 """ Process a reply for a successful execution request.
739 """
739 """
740 payload = msg['content']['payload']
740 payload = msg['content']['payload']
741 for item in payload:
741 for item in payload:
742 if not self._process_execute_payload(item):
742 if not self._process_execute_payload(item):
743 warning = 'Warning: received unknown payload of type %s'
743 warning = 'Warning: received unknown payload of type %s'
744 print(warning % repr(item['source']))
744 print(warning % repr(item['source']))
745
745
746 def _process_execute_payload(self, item):
746 def _process_execute_payload(self, item):
747 """ Process a single payload item from the list of payload items in an
747 """ Process a single payload item from the list of payload items in an
748 execution reply. Returns whether the payload was handled.
748 execution reply. Returns whether the payload was handled.
749 """
749 """
750 # The basic FrontendWidget doesn't handle payloads, as they are a
750 # The basic FrontendWidget doesn't handle payloads, as they are a
751 # mechanism for going beyond the standard Python interpreter model.
751 # mechanism for going beyond the standard Python interpreter model.
752 return False
752 return False
753
753
754 def _show_interpreter_prompt(self):
754 def _show_interpreter_prompt(self):
755 """ Shows a prompt for the interpreter.
755 """ Shows a prompt for the interpreter.
756 """
756 """
757 self._show_prompt('>>> ')
757 self._show_prompt('>>> ')
758
758
759 def _show_interpreter_prompt_for_reply(self, msg):
759 def _show_interpreter_prompt_for_reply(self, msg):
760 """ Shows a prompt for the interpreter given an 'execute_reply' message.
760 """ Shows a prompt for the interpreter given an 'execute_reply' message.
761 """
761 """
762 self._show_interpreter_prompt()
762 self._show_interpreter_prompt()
763
763
764 #------ Signal handlers ----------------------------------------------------
764 #------ Signal handlers ----------------------------------------------------
765
765
766 def _document_contents_change(self, position, removed, added):
766 def _document_contents_change(self, position, removed, added):
767 """ Called whenever the document's content changes. Display a call tip
767 """ Called whenever the document's content changes. Display a call tip
768 if appropriate.
768 if appropriate.
769 """
769 """
770 # Calculate where the cursor should be *after* the change:
770 # Calculate where the cursor should be *after* the change:
771 position += added
771 position += added
772
772
773 document = self._control.document()
773 document = self._control.document()
774 if position == self._get_cursor().position():
774 if position == self._get_cursor().position():
775 self._call_tip()
775 self._call_tip()
776
776
777 #------ Trait default initializers -----------------------------------------
777 #------ Trait default initializers -----------------------------------------
778
778
779 def _banner_default(self):
779 def _banner_default(self):
780 """ Returns the standard Python banner.
780 """ Returns the standard Python banner.
781 """
781 """
782 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
782 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
783 '"license" for more information.'
783 '"license" for more information.'
784 return banner % (sys.version, sys.platform)
784 return banner % (sys.version, sys.platform)
@@ -1,304 +1,304 b''
1 # System library imports
1 # System library imports
2 from IPython.external.qt import QtGui
2 from IPython.external.qt import QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.utils.traitlets import Bool
5 from IPython.utils.traitlets import Bool
6 from console_widget import ConsoleWidget
6 from .console_widget import ConsoleWidget
7
7
8
8
9 class HistoryConsoleWidget(ConsoleWidget):
9 class HistoryConsoleWidget(ConsoleWidget):
10 """ A ConsoleWidget that keeps a history of the commands that have been
10 """ A ConsoleWidget that keeps a history of the commands that have been
11 executed and provides a readline-esque interface to this history.
11 executed and provides a readline-esque interface to this history.
12 """
12 """
13
13
14 #------ Configuration ------------------------------------------------------
14 #------ Configuration ------------------------------------------------------
15
15
16 # If enabled, the input buffer will become "locked" to history movement when
16 # If enabled, the input buffer will become "locked" to history movement when
17 # an edit is made to a multi-line input buffer. To override the lock, use
17 # an edit is made to a multi-line input buffer. To override the lock, use
18 # Shift in conjunction with the standard history cycling keys.
18 # Shift in conjunction with the standard history cycling keys.
19 history_lock = Bool(False, config=True)
19 history_lock = Bool(False, config=True)
20
20
21 #---------------------------------------------------------------------------
21 #---------------------------------------------------------------------------
22 # 'object' interface
22 # 'object' interface
23 #---------------------------------------------------------------------------
23 #---------------------------------------------------------------------------
24
24
25 def __init__(self, *args, **kw):
25 def __init__(self, *args, **kw):
26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
27
27
28 # HistoryConsoleWidget protected variables.
28 # HistoryConsoleWidget protected variables.
29 self._history = []
29 self._history = []
30 self._history_edits = {}
30 self._history_edits = {}
31 self._history_index = 0
31 self._history_index = 0
32 self._history_prefix = ''
32 self._history_prefix = ''
33
33
34 #---------------------------------------------------------------------------
34 #---------------------------------------------------------------------------
35 # 'ConsoleWidget' public interface
35 # 'ConsoleWidget' public interface
36 #---------------------------------------------------------------------------
36 #---------------------------------------------------------------------------
37
37
38 def execute(self, source=None, hidden=False, interactive=False):
38 def execute(self, source=None, hidden=False, interactive=False):
39 """ Reimplemented to the store history.
39 """ Reimplemented to the store history.
40 """
40 """
41 if not hidden:
41 if not hidden:
42 history = self.input_buffer if source is None else source
42 history = self.input_buffer if source is None else source
43
43
44 executed = super(HistoryConsoleWidget, self).execute(
44 executed = super(HistoryConsoleWidget, self).execute(
45 source, hidden, interactive)
45 source, hidden, interactive)
46
46
47 if executed and not hidden:
47 if executed and not hidden:
48 # Save the command unless it was an empty string or was identical
48 # Save the command unless it was an empty string or was identical
49 # to the previous command.
49 # to the previous command.
50 history = history.rstrip()
50 history = history.rstrip()
51 if history and (not self._history or self._history[-1] != history):
51 if history and (not self._history or self._history[-1] != history):
52 self._history.append(history)
52 self._history.append(history)
53
53
54 # Emulate readline: reset all history edits.
54 # Emulate readline: reset all history edits.
55 self._history_edits = {}
55 self._history_edits = {}
56
56
57 # Move the history index to the most recent item.
57 # Move the history index to the most recent item.
58 self._history_index = len(self._history)
58 self._history_index = len(self._history)
59
59
60 return executed
60 return executed
61
61
62 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
63 # 'ConsoleWidget' abstract interface
63 # 'ConsoleWidget' abstract interface
64 #---------------------------------------------------------------------------
64 #---------------------------------------------------------------------------
65
65
66 def _up_pressed(self, shift_modifier):
66 def _up_pressed(self, shift_modifier):
67 """ Called when the up key is pressed. Returns whether to continue
67 """ Called when the up key is pressed. Returns whether to continue
68 processing the event.
68 processing the event.
69 """
69 """
70 prompt_cursor = self._get_prompt_cursor()
70 prompt_cursor = self._get_prompt_cursor()
71 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
71 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
72 # Bail out if we're locked.
72 # Bail out if we're locked.
73 if self._history_locked() and not shift_modifier:
73 if self._history_locked() and not shift_modifier:
74 return False
74 return False
75
75
76 # Set a search prefix based on the cursor position.
76 # Set a search prefix based on the cursor position.
77 col = self._get_input_buffer_cursor_column()
77 col = self._get_input_buffer_cursor_column()
78 input_buffer = self.input_buffer
78 input_buffer = self.input_buffer
79 # use the *shortest* of the cursor column and the history prefix
79 # use the *shortest* of the cursor column and the history prefix
80 # to determine if the prefix has changed
80 # to determine if the prefix has changed
81 n = min(col, len(self._history_prefix))
81 n = min(col, len(self._history_prefix))
82
82
83 # prefix changed, restart search from the beginning
83 # prefix changed, restart search from the beginning
84 if (self._history_prefix[:n] != input_buffer[:n]):
84 if (self._history_prefix[:n] != input_buffer[:n]):
85 self._history_index = len(self._history)
85 self._history_index = len(self._history)
86
86
87 # the only time we shouldn't set the history prefix
87 # the only time we shouldn't set the history prefix
88 # to the line up to the cursor is if we are already
88 # to the line up to the cursor is if we are already
89 # in a simple scroll (no prefix),
89 # in a simple scroll (no prefix),
90 # and the cursor is at the end of the first line
90 # and the cursor is at the end of the first line
91
91
92 # check if we are at the end of the first line
92 # check if we are at the end of the first line
93 c = self._get_cursor()
93 c = self._get_cursor()
94 current_pos = c.position()
94 current_pos = c.position()
95 c.movePosition(QtGui.QTextCursor.EndOfLine)
95 c.movePosition(QtGui.QTextCursor.EndOfLine)
96 at_eol = (c.position() == current_pos)
96 at_eol = (c.position() == current_pos)
97
97
98 if self._history_index == len(self._history) or \
98 if self._history_index == len(self._history) or \
99 not (self._history_prefix == '' and at_eol) or \
99 not (self._history_prefix == '' and at_eol) or \
100 not (self._get_edited_history(self._history_index)[:col] == input_buffer[:col]):
100 not (self._get_edited_history(self._history_index)[:col] == input_buffer[:col]):
101 self._history_prefix = input_buffer[:col]
101 self._history_prefix = input_buffer[:col]
102
102
103 # Perform the search.
103 # Perform the search.
104 self.history_previous(self._history_prefix,
104 self.history_previous(self._history_prefix,
105 as_prefix=not shift_modifier)
105 as_prefix=not shift_modifier)
106
106
107 # Go to the first line of the prompt for seemless history scrolling.
107 # Go to the first line of the prompt for seemless history scrolling.
108 # Emulate readline: keep the cursor position fixed for a prefix
108 # Emulate readline: keep the cursor position fixed for a prefix
109 # search.
109 # search.
110 cursor = self._get_prompt_cursor()
110 cursor = self._get_prompt_cursor()
111 if self._history_prefix:
111 if self._history_prefix:
112 cursor.movePosition(QtGui.QTextCursor.Right,
112 cursor.movePosition(QtGui.QTextCursor.Right,
113 n=len(self._history_prefix))
113 n=len(self._history_prefix))
114 else:
114 else:
115 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
115 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
116 self._set_cursor(cursor)
116 self._set_cursor(cursor)
117
117
118 return False
118 return False
119
119
120 return True
120 return True
121
121
122 def _down_pressed(self, shift_modifier):
122 def _down_pressed(self, shift_modifier):
123 """ Called when the down key is pressed. Returns whether to continue
123 """ Called when the down key is pressed. Returns whether to continue
124 processing the event.
124 processing the event.
125 """
125 """
126 end_cursor = self._get_end_cursor()
126 end_cursor = self._get_end_cursor()
127 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
127 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
128 # Bail out if we're locked.
128 # Bail out if we're locked.
129 if self._history_locked() and not shift_modifier:
129 if self._history_locked() and not shift_modifier:
130 return False
130 return False
131
131
132 # Perform the search.
132 # Perform the search.
133 replaced = self.history_next(self._history_prefix,
133 replaced = self.history_next(self._history_prefix,
134 as_prefix=not shift_modifier)
134 as_prefix=not shift_modifier)
135
135
136 # Emulate readline: keep the cursor position fixed for a prefix
136 # Emulate readline: keep the cursor position fixed for a prefix
137 # search. (We don't need to move the cursor to the end of the buffer
137 # search. (We don't need to move the cursor to the end of the buffer
138 # in the other case because this happens automatically when the
138 # in the other case because this happens automatically when the
139 # input buffer is set.)
139 # input buffer is set.)
140 if self._history_prefix and replaced:
140 if self._history_prefix and replaced:
141 cursor = self._get_prompt_cursor()
141 cursor = self._get_prompt_cursor()
142 cursor.movePosition(QtGui.QTextCursor.Right,
142 cursor.movePosition(QtGui.QTextCursor.Right,
143 n=len(self._history_prefix))
143 n=len(self._history_prefix))
144 self._set_cursor(cursor)
144 self._set_cursor(cursor)
145
145
146 return False
146 return False
147
147
148 return True
148 return True
149
149
150 #---------------------------------------------------------------------------
150 #---------------------------------------------------------------------------
151 # 'HistoryConsoleWidget' public interface
151 # 'HistoryConsoleWidget' public interface
152 #---------------------------------------------------------------------------
152 #---------------------------------------------------------------------------
153
153
154 def history_previous(self, substring='', as_prefix=True):
154 def history_previous(self, substring='', as_prefix=True):
155 """ If possible, set the input buffer to a previous history item.
155 """ If possible, set the input buffer to a previous history item.
156
156
157 Parameters:
157 Parameters:
158 -----------
158 -----------
159 substring : str, optional
159 substring : str, optional
160 If specified, search for an item with this substring.
160 If specified, search for an item with this substring.
161 as_prefix : bool, optional
161 as_prefix : bool, optional
162 If True, the substring must match at the beginning (default).
162 If True, the substring must match at the beginning (default).
163
163
164 Returns:
164 Returns:
165 --------
165 --------
166 Whether the input buffer was changed.
166 Whether the input buffer was changed.
167 """
167 """
168 index = self._history_index
168 index = self._history_index
169 replace = False
169 replace = False
170 while index > 0:
170 while index > 0:
171 index -= 1
171 index -= 1
172 history = self._get_edited_history(index)
172 history = self._get_edited_history(index)
173 if (as_prefix and history.startswith(substring)) \
173 if (as_prefix and history.startswith(substring)) \
174 or (not as_prefix and substring in history):
174 or (not as_prefix and substring in history):
175 replace = True
175 replace = True
176 break
176 break
177
177
178 if replace:
178 if replace:
179 self._store_edits()
179 self._store_edits()
180 self._history_index = index
180 self._history_index = index
181 self.input_buffer = history
181 self.input_buffer = history
182
182
183 return replace
183 return replace
184
184
185 def history_next(self, substring='', as_prefix=True):
185 def history_next(self, substring='', as_prefix=True):
186 """ If possible, set the input buffer to a subsequent history item.
186 """ If possible, set the input buffer to a subsequent history item.
187
187
188 Parameters:
188 Parameters:
189 -----------
189 -----------
190 substring : str, optional
190 substring : str, optional
191 If specified, search for an item with this substring.
191 If specified, search for an item with this substring.
192 as_prefix : bool, optional
192 as_prefix : bool, optional
193 If True, the substring must match at the beginning (default).
193 If True, the substring must match at the beginning (default).
194
194
195 Returns:
195 Returns:
196 --------
196 --------
197 Whether the input buffer was changed.
197 Whether the input buffer was changed.
198 """
198 """
199 index = self._history_index
199 index = self._history_index
200 replace = False
200 replace = False
201 while index < len(self._history):
201 while index < len(self._history):
202 index += 1
202 index += 1
203 history = self._get_edited_history(index)
203 history = self._get_edited_history(index)
204 if (as_prefix and history.startswith(substring)) \
204 if (as_prefix and history.startswith(substring)) \
205 or (not as_prefix and substring in history):
205 or (not as_prefix and substring in history):
206 replace = True
206 replace = True
207 break
207 break
208
208
209 if replace:
209 if replace:
210 self._store_edits()
210 self._store_edits()
211 self._history_index = index
211 self._history_index = index
212 self.input_buffer = history
212 self.input_buffer = history
213
213
214 return replace
214 return replace
215
215
216 def history_tail(self, n=10):
216 def history_tail(self, n=10):
217 """ Get the local history list.
217 """ Get the local history list.
218
218
219 Parameters:
219 Parameters:
220 -----------
220 -----------
221 n : int
221 n : int
222 The (maximum) number of history items to get.
222 The (maximum) number of history items to get.
223 """
223 """
224 return self._history[-n:]
224 return self._history[-n:]
225
225
226 def _request_update_session_history_length(self):
226 def _request_update_session_history_length(self):
227 msg_id = self.kernel_client.shell_channel.execute('',
227 msg_id = self.kernel_client.shell_channel.execute('',
228 silent=True,
228 silent=True,
229 user_expressions={
229 user_expressions={
230 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
230 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
231 }
231 }
232 )
232 )
233 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'save_magic')
233 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'save_magic')
234
234
235 def _handle_execute_reply(self, msg):
235 def _handle_execute_reply(self, msg):
236 """ Handles replies for code execution, here only session history length
236 """ Handles replies for code execution, here only session history length
237 """
237 """
238 msg_id = msg['parent_header']['msg_id']
238 msg_id = msg['parent_header']['msg_id']
239 info = self._request_info['execute'].pop(msg_id,None)
239 info = self._request_info['execute'].pop(msg_id,None)
240 if info and info.kind == 'save_magic' and not self._hidden:
240 if info and info.kind == 'save_magic' and not self._hidden:
241 content = msg['content']
241 content = msg['content']
242 status = content['status']
242 status = content['status']
243 if status == 'ok':
243 if status == 'ok':
244 self._max_session_history = int(
244 self._max_session_history = int(
245 content['user_expressions']['hlen']['data']['text/plain']
245 content['user_expressions']['hlen']['data']['text/plain']
246 )
246 )
247
247
248 def save_magic(self):
248 def save_magic(self):
249 # update the session history length
249 # update the session history length
250 self._request_update_session_history_length()
250 self._request_update_session_history_length()
251
251
252 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
252 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
253 "Enter A filename",
253 "Enter A filename",
254 filter='Python File (*.py);; All files (*.*)'
254 filter='Python File (*.py);; All files (*.*)'
255 )
255 )
256
256
257 # let's the user search/type for a file name, while the history length
257 # let's the user search/type for a file name, while the history length
258 # is fetched
258 # is fetched
259
259
260 if file_name:
260 if file_name:
261 hist_range, ok = QtGui.QInputDialog.getText(self,
261 hist_range, ok = QtGui.QInputDialog.getText(self,
262 'Please enter an interval of command to save',
262 'Please enter an interval of command to save',
263 'Saving commands:',
263 'Saving commands:',
264 text=str('1-'+str(self._max_session_history))
264 text=str('1-'+str(self._max_session_history))
265 )
265 )
266 if ok:
266 if ok:
267 self.execute("%save"+" "+file_name+" "+str(hist_range))
267 self.execute("%save"+" "+file_name+" "+str(hist_range))
268
268
269 #---------------------------------------------------------------------------
269 #---------------------------------------------------------------------------
270 # 'HistoryConsoleWidget' protected interface
270 # 'HistoryConsoleWidget' protected interface
271 #---------------------------------------------------------------------------
271 #---------------------------------------------------------------------------
272
272
273 def _history_locked(self):
273 def _history_locked(self):
274 """ Returns whether history movement is locked.
274 """ Returns whether history movement is locked.
275 """
275 """
276 return (self.history_lock and
276 return (self.history_lock and
277 (self._get_edited_history(self._history_index) !=
277 (self._get_edited_history(self._history_index) !=
278 self.input_buffer) and
278 self.input_buffer) and
279 (self._get_prompt_cursor().blockNumber() !=
279 (self._get_prompt_cursor().blockNumber() !=
280 self._get_end_cursor().blockNumber()))
280 self._get_end_cursor().blockNumber()))
281
281
282 def _get_edited_history(self, index):
282 def _get_edited_history(self, index):
283 """ Retrieves a history item, possibly with temporary edits.
283 """ Retrieves a history item, possibly with temporary edits.
284 """
284 """
285 if index in self._history_edits:
285 if index in self._history_edits:
286 return self._history_edits[index]
286 return self._history_edits[index]
287 elif index == len(self._history):
287 elif index == len(self._history):
288 return unicode()
288 return unicode()
289 return self._history[index]
289 return self._history[index]
290
290
291 def _set_history(self, history):
291 def _set_history(self, history):
292 """ Replace the current history with a sequence of history items.
292 """ Replace the current history with a sequence of history items.
293 """
293 """
294 self._history = list(history)
294 self._history = list(history)
295 self._history_edits = {}
295 self._history_edits = {}
296 self._history_index = len(self._history)
296 self._history_index = len(self._history)
297
297
298 def _store_edits(self):
298 def _store_edits(self):
299 """ If there are edits to the current input buffer, store them.
299 """ If there are edits to the current input buffer, store them.
300 """
300 """
301 current = self.input_buffer
301 current = self.input_buffer
302 if self._history_index == len(self._history) or \
302 if self._history_index == len(self._history) or \
303 self._history[self._history_index] != current:
303 self._history[self._history_index] != current:
304 self._history_edits[self._history_index] = current
304 self._history_edits[self._history_index] = current
@@ -1,584 +1,584 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Imports
6 # Imports
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard library imports
9 # Standard library imports
10 from collections import namedtuple
10 from collections import namedtuple
11 import os.path
11 import os.path
12 import re
12 import re
13 from subprocess import Popen
13 from subprocess import Popen
14 import sys
14 import sys
15 import time
15 import time
16 from textwrap import dedent
16 from textwrap import dedent
17
17
18 # System library imports
18 # System library imports
19 from IPython.external.qt import QtCore, QtGui
19 from IPython.external.qt import QtCore, QtGui
20
20
21 # Local imports
21 # Local imports
22 from IPython.core.inputsplitter import IPythonInputSplitter
22 from IPython.core.inputsplitter import IPythonInputSplitter
23 from IPython.core.inputtransformer import ipy_prompt
23 from IPython.core.inputtransformer import ipy_prompt
24 from IPython.utils.traitlets import Bool, Unicode
24 from IPython.utils.traitlets import Bool, Unicode
25 from frontend_widget import FrontendWidget
25 from .frontend_widget import FrontendWidget
26 import styles
26 from . import styles
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Constants
29 # Constants
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 # Default strings to build and display input and output prompts (and separators
32 # Default strings to build and display input and output prompts (and separators
33 # in between)
33 # in between)
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_input_sep = '\n'
36 default_input_sep = '\n'
37 default_output_sep = ''
37 default_output_sep = ''
38 default_output_sep2 = ''
38 default_output_sep2 = ''
39
39
40 # Base path for most payload sources.
40 # Base path for most payload sources.
41 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
41 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
42
42
43 if sys.platform.startswith('win'):
43 if sys.platform.startswith('win'):
44 default_editor = 'notepad'
44 default_editor = 'notepad'
45 else:
45 else:
46 default_editor = ''
46 default_editor = ''
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # IPythonWidget class
49 # IPythonWidget class
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 class IPythonWidget(FrontendWidget):
52 class IPythonWidget(FrontendWidget):
53 """ A FrontendWidget for an IPython kernel.
53 """ A FrontendWidget for an IPython kernel.
54 """
54 """
55
55
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # settings.
58 # settings.
59 custom_edit = Bool(False)
59 custom_edit = Bool(False)
60 custom_edit_requested = QtCore.Signal(object, object)
60 custom_edit_requested = QtCore.Signal(object, object)
61
61
62 editor = Unicode(default_editor, config=True,
62 editor = Unicode(default_editor, config=True,
63 help="""
63 help="""
64 A command for invoking a system text editor. If the string contains a
64 A command for invoking a system text editor. If the string contains a
65 {filename} format specifier, it will be used. Otherwise, the filename
65 {filename} format specifier, it will be used. Otherwise, the filename
66 will be appended to the end the command.
66 will be appended to the end the command.
67 """)
67 """)
68
68
69 editor_line = Unicode(config=True,
69 editor_line = Unicode(config=True,
70 help="""
70 help="""
71 The editor command to use when a specific line number is requested. The
71 The editor command to use when a specific line number is requested. The
72 string should contain two format specifiers: {line} and {filename}. If
72 string should contain two format specifiers: {line} and {filename}. If
73 this parameter is not specified, the line number option to the %edit
73 this parameter is not specified, the line number option to the %edit
74 magic will be ignored.
74 magic will be ignored.
75 """)
75 """)
76
76
77 style_sheet = Unicode(config=True,
77 style_sheet = Unicode(config=True,
78 help="""
78 help="""
79 A CSS stylesheet. The stylesheet can contain classes for:
79 A CSS stylesheet. The stylesheet can contain classes for:
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 3. IPython: .error, .in-prompt, .out-prompt, etc
82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 """)
83 """)
84
84
85 syntax_style = Unicode(config=True,
85 syntax_style = Unicode(config=True,
86 help="""
86 help="""
87 If not empty, use this Pygments style for syntax highlighting.
87 If not empty, use this Pygments style for syntax highlighting.
88 Otherwise, the style sheet is queried for Pygments style
88 Otherwise, the style sheet is queried for Pygments style
89 information.
89 information.
90 """)
90 """)
91
91
92 # Prompts.
92 # Prompts.
93 in_prompt = Unicode(default_in_prompt, config=True)
93 in_prompt = Unicode(default_in_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
98
98
99 # FrontendWidget protected class variables.
99 # FrontendWidget protected class variables.
100 _input_splitter_class = IPythonInputSplitter
100 _input_splitter_class = IPythonInputSplitter
101 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
101 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
102 logical_line_transforms=[],
102 logical_line_transforms=[],
103 python_line_transforms=[],
103 python_line_transforms=[],
104 )
104 )
105
105
106 # IPythonWidget protected class variables.
106 # IPythonWidget protected class variables.
107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
108 _payload_source_edit = 'edit_magic'
108 _payload_source_edit = 'edit_magic'
109 _payload_source_exit = 'ask_exit'
109 _payload_source_exit = 'ask_exit'
110 _payload_source_next_input = 'set_next_input'
110 _payload_source_next_input = 'set_next_input'
111 _payload_source_page = 'page'
111 _payload_source_page = 'page'
112 _retrying_history_request = False
112 _retrying_history_request = False
113
113
114 #---------------------------------------------------------------------------
114 #---------------------------------------------------------------------------
115 # 'object' interface
115 # 'object' interface
116 #---------------------------------------------------------------------------
116 #---------------------------------------------------------------------------
117
117
118 def __init__(self, *args, **kw):
118 def __init__(self, *args, **kw):
119 super(IPythonWidget, self).__init__(*args, **kw)
119 super(IPythonWidget, self).__init__(*args, **kw)
120
120
121 # IPythonWidget protected variables.
121 # IPythonWidget protected variables.
122 self._payload_handlers = {
122 self._payload_handlers = {
123 self._payload_source_edit : self._handle_payload_edit,
123 self._payload_source_edit : self._handle_payload_edit,
124 self._payload_source_exit : self._handle_payload_exit,
124 self._payload_source_exit : self._handle_payload_exit,
125 self._payload_source_page : self._handle_payload_page,
125 self._payload_source_page : self._handle_payload_page,
126 self._payload_source_next_input : self._handle_payload_next_input }
126 self._payload_source_next_input : self._handle_payload_next_input }
127 self._previous_prompt_obj = None
127 self._previous_prompt_obj = None
128 self._keep_kernel_on_exit = None
128 self._keep_kernel_on_exit = None
129
129
130 # Initialize widget styling.
130 # Initialize widget styling.
131 if self.style_sheet:
131 if self.style_sheet:
132 self._style_sheet_changed()
132 self._style_sheet_changed()
133 self._syntax_style_changed()
133 self._syntax_style_changed()
134 else:
134 else:
135 self.set_default_style()
135 self.set_default_style()
136
136
137 #---------------------------------------------------------------------------
137 #---------------------------------------------------------------------------
138 # 'BaseFrontendMixin' abstract interface
138 # 'BaseFrontendMixin' abstract interface
139 #---------------------------------------------------------------------------
139 #---------------------------------------------------------------------------
140
140
141 def _handle_complete_reply(self, rep):
141 def _handle_complete_reply(self, rep):
142 """ Reimplemented to support IPython's improved completion machinery.
142 """ Reimplemented to support IPython's improved completion machinery.
143 """
143 """
144 self.log.debug("complete: %s", rep.get('content', ''))
144 self.log.debug("complete: %s", rep.get('content', ''))
145 cursor = self._get_cursor()
145 cursor = self._get_cursor()
146 info = self._request_info.get('complete')
146 info = self._request_info.get('complete')
147 if info and info.id == rep['parent_header']['msg_id'] and \
147 if info and info.id == rep['parent_header']['msg_id'] and \
148 info.pos == cursor.position():
148 info.pos == cursor.position():
149 matches = rep['content']['matches']
149 matches = rep['content']['matches']
150 text = rep['content']['matched_text']
150 text = rep['content']['matched_text']
151 offset = len(text)
151 offset = len(text)
152
152
153 # Clean up matches with period and path separators if the matched
153 # Clean up matches with period and path separators if the matched
154 # text has not been transformed. This is done by truncating all
154 # text has not been transformed. This is done by truncating all
155 # but the last component and then suitably decreasing the offset
155 # but the last component and then suitably decreasing the offset
156 # between the current cursor position and the start of completion.
156 # between the current cursor position and the start of completion.
157 if len(matches) > 1 and matches[0][:offset] == text:
157 if len(matches) > 1 and matches[0][:offset] == text:
158 parts = re.split(r'[./\\]', text)
158 parts = re.split(r'[./\\]', text)
159 sep_count = len(parts) - 1
159 sep_count = len(parts) - 1
160 if sep_count:
160 if sep_count:
161 chop_length = sum(map(len, parts[:sep_count])) + sep_count
161 chop_length = sum(map(len, parts[:sep_count])) + sep_count
162 matches = [ match[chop_length:] for match in matches ]
162 matches = [ match[chop_length:] for match in matches ]
163 offset -= chop_length
163 offset -= chop_length
164
164
165 # Move the cursor to the start of the match and complete.
165 # Move the cursor to the start of the match and complete.
166 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
166 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
167 self._complete_with_items(cursor, matches)
167 self._complete_with_items(cursor, matches)
168
168
169 def _handle_execute_reply(self, msg):
169 def _handle_execute_reply(self, msg):
170 """ Reimplemented to support prompt requests.
170 """ Reimplemented to support prompt requests.
171 """
171 """
172 msg_id = msg['parent_header'].get('msg_id')
172 msg_id = msg['parent_header'].get('msg_id')
173 info = self._request_info['execute'].get(msg_id)
173 info = self._request_info['execute'].get(msg_id)
174 if info and info.kind == 'prompt':
174 if info and info.kind == 'prompt':
175 number = msg['content']['execution_count'] + 1
175 number = msg['content']['execution_count'] + 1
176 self._show_interpreter_prompt(number)
176 self._show_interpreter_prompt(number)
177 self._request_info['execute'].pop(msg_id)
177 self._request_info['execute'].pop(msg_id)
178 else:
178 else:
179 super(IPythonWidget, self)._handle_execute_reply(msg)
179 super(IPythonWidget, self)._handle_execute_reply(msg)
180
180
181 def _handle_history_reply(self, msg):
181 def _handle_history_reply(self, msg):
182 """ Implemented to handle history tail replies, which are only supported
182 """ Implemented to handle history tail replies, which are only supported
183 by the IPython kernel.
183 by the IPython kernel.
184 """
184 """
185 content = msg['content']
185 content = msg['content']
186 if 'history' not in content:
186 if 'history' not in content:
187 self.log.error("History request failed: %r"%content)
187 self.log.error("History request failed: %r"%content)
188 if content.get('status', '') == 'aborted' and \
188 if content.get('status', '') == 'aborted' and \
189 not self._retrying_history_request:
189 not self._retrying_history_request:
190 # a *different* action caused this request to be aborted, so
190 # a *different* action caused this request to be aborted, so
191 # we should try again.
191 # we should try again.
192 self.log.error("Retrying aborted history request")
192 self.log.error("Retrying aborted history request")
193 # prevent multiple retries of aborted requests:
193 # prevent multiple retries of aborted requests:
194 self._retrying_history_request = True
194 self._retrying_history_request = True
195 # wait out the kernel's queue flush, which is currently timed at 0.1s
195 # wait out the kernel's queue flush, which is currently timed at 0.1s
196 time.sleep(0.25)
196 time.sleep(0.25)
197 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
197 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
198 else:
198 else:
199 self._retrying_history_request = False
199 self._retrying_history_request = False
200 return
200 return
201 # reset retry flag
201 # reset retry flag
202 self._retrying_history_request = False
202 self._retrying_history_request = False
203 history_items = content['history']
203 history_items = content['history']
204 self.log.debug("Received history reply with %i entries", len(history_items))
204 self.log.debug("Received history reply with %i entries", len(history_items))
205 items = []
205 items = []
206 last_cell = u""
206 last_cell = u""
207 for _, _, cell in history_items:
207 for _, _, cell in history_items:
208 cell = cell.rstrip()
208 cell = cell.rstrip()
209 if cell != last_cell:
209 if cell != last_cell:
210 items.append(cell)
210 items.append(cell)
211 last_cell = cell
211 last_cell = cell
212 self._set_history(items)
212 self._set_history(items)
213
213
214 def _handle_pyout(self, msg):
214 def _handle_pyout(self, msg):
215 """ Reimplemented for IPython-style "display hook".
215 """ Reimplemented for IPython-style "display hook".
216 """
216 """
217 self.log.debug("pyout: %s", msg.get('content', ''))
217 self.log.debug("pyout: %s", msg.get('content', ''))
218 if not self._hidden and self._is_from_this_session(msg):
218 if not self._hidden and self._is_from_this_session(msg):
219 content = msg['content']
219 content = msg['content']
220 prompt_number = content.get('execution_count', 0)
220 prompt_number = content.get('execution_count', 0)
221 data = content['data']
221 data = content['data']
222 if 'text/html' in data:
222 if 'text/html' in data:
223 self._append_plain_text(self.output_sep, True)
223 self._append_plain_text(self.output_sep, True)
224 self._append_html(self._make_out_prompt(prompt_number), True)
224 self._append_html(self._make_out_prompt(prompt_number), True)
225 html = data['text/html']
225 html = data['text/html']
226 self._append_plain_text('\n', True)
226 self._append_plain_text('\n', True)
227 self._append_html(html + self.output_sep2, True)
227 self._append_html(html + self.output_sep2, True)
228 elif 'text/plain' in data:
228 elif 'text/plain' in data:
229 self._append_plain_text(self.output_sep, True)
229 self._append_plain_text(self.output_sep, True)
230 self._append_html(self._make_out_prompt(prompt_number), True)
230 self._append_html(self._make_out_prompt(prompt_number), True)
231 text = data['text/plain']
231 text = data['text/plain']
232 # If the repr is multiline, make sure we start on a new line,
232 # If the repr is multiline, make sure we start on a new line,
233 # so that its lines are aligned.
233 # so that its lines are aligned.
234 if "\n" in text and not self.output_sep.endswith("\n"):
234 if "\n" in text and not self.output_sep.endswith("\n"):
235 self._append_plain_text('\n', True)
235 self._append_plain_text('\n', True)
236 self._append_plain_text(text + self.output_sep2, True)
236 self._append_plain_text(text + self.output_sep2, True)
237
237
238 def _handle_display_data(self, msg):
238 def _handle_display_data(self, msg):
239 """ The base handler for the ``display_data`` message.
239 """ The base handler for the ``display_data`` message.
240 """
240 """
241 self.log.debug("display: %s", msg.get('content', ''))
241 self.log.debug("display: %s", msg.get('content', ''))
242 # For now, we don't display data from other frontends, but we
242 # For now, we don't display data from other frontends, but we
243 # eventually will as this allows all frontends to monitor the display
243 # eventually will as this allows all frontends to monitor the display
244 # data. But we need to figure out how to handle this in the GUI.
244 # data. But we need to figure out how to handle this in the GUI.
245 if not self._hidden and self._is_from_this_session(msg):
245 if not self._hidden and self._is_from_this_session(msg):
246 source = msg['content']['source']
246 source = msg['content']['source']
247 data = msg['content']['data']
247 data = msg['content']['data']
248 metadata = msg['content']['metadata']
248 metadata = msg['content']['metadata']
249 # In the regular IPythonWidget, we simply print the plain text
249 # In the regular IPythonWidget, we simply print the plain text
250 # representation.
250 # representation.
251 if 'text/html' in data:
251 if 'text/html' in data:
252 html = data['text/html']
252 html = data['text/html']
253 self._append_html(html, True)
253 self._append_html(html, True)
254 elif 'text/plain' in data:
254 elif 'text/plain' in data:
255 text = data['text/plain']
255 text = data['text/plain']
256 self._append_plain_text(text, True)
256 self._append_plain_text(text, True)
257 # This newline seems to be needed for text and html output.
257 # This newline seems to be needed for text and html output.
258 self._append_plain_text(u'\n', True)
258 self._append_plain_text(u'\n', True)
259
259
260 def _started_channels(self):
260 def _started_channels(self):
261 """Reimplemented to make a history request and load %guiref."""
261 """Reimplemented to make a history request and load %guiref."""
262 super(IPythonWidget, self)._started_channels()
262 super(IPythonWidget, self)._started_channels()
263 self._load_guiref_magic()
263 self._load_guiref_magic()
264 self.kernel_client.shell_channel.history(hist_access_type='tail',
264 self.kernel_client.shell_channel.history(hist_access_type='tail',
265 n=1000)
265 n=1000)
266
266
267 def _started_kernel(self):
267 def _started_kernel(self):
268 """Load %guiref when the kernel starts (if channels are also started).
268 """Load %guiref when the kernel starts (if channels are also started).
269
269
270 Principally triggered by kernel restart.
270 Principally triggered by kernel restart.
271 """
271 """
272 if self.kernel_client.shell_channel is not None:
272 if self.kernel_client.shell_channel is not None:
273 self._load_guiref_magic()
273 self._load_guiref_magic()
274
274
275 def _load_guiref_magic(self):
275 def _load_guiref_magic(self):
276 """Load %guiref magic."""
276 """Load %guiref magic."""
277 self.kernel_client.shell_channel.execute('\n'.join([
277 self.kernel_client.shell_channel.execute('\n'.join([
278 "try:",
278 "try:",
279 " _usage",
279 " _usage",
280 "except:",
280 "except:",
281 " from IPython.core import usage as _usage",
281 " from IPython.core import usage as _usage",
282 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
282 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
283 " del _usage",
283 " del _usage",
284 ]), silent=True)
284 ]), silent=True)
285
285
286 #---------------------------------------------------------------------------
286 #---------------------------------------------------------------------------
287 # 'ConsoleWidget' public interface
287 # 'ConsoleWidget' public interface
288 #---------------------------------------------------------------------------
288 #---------------------------------------------------------------------------
289
289
290 #---------------------------------------------------------------------------
290 #---------------------------------------------------------------------------
291 # 'FrontendWidget' public interface
291 # 'FrontendWidget' public interface
292 #---------------------------------------------------------------------------
292 #---------------------------------------------------------------------------
293
293
294 def execute_file(self, path, hidden=False):
294 def execute_file(self, path, hidden=False):
295 """ Reimplemented to use the 'run' magic.
295 """ Reimplemented to use the 'run' magic.
296 """
296 """
297 # Use forward slashes on Windows to avoid escaping each separator.
297 # Use forward slashes on Windows to avoid escaping each separator.
298 if sys.platform == 'win32':
298 if sys.platform == 'win32':
299 path = os.path.normpath(path).replace('\\', '/')
299 path = os.path.normpath(path).replace('\\', '/')
300
300
301 # Perhaps we should not be using %run directly, but while we
301 # Perhaps we should not be using %run directly, but while we
302 # are, it is necessary to quote or escape filenames containing spaces
302 # are, it is necessary to quote or escape filenames containing spaces
303 # or quotes.
303 # or quotes.
304
304
305 # In earlier code here, to minimize escaping, we sometimes quoted the
305 # In earlier code here, to minimize escaping, we sometimes quoted the
306 # filename with single quotes. But to do this, this code must be
306 # filename with single quotes. But to do this, this code must be
307 # platform-aware, because run uses shlex rather than python string
307 # platform-aware, because run uses shlex rather than python string
308 # parsing, so that:
308 # parsing, so that:
309 # * In Win: single quotes can be used in the filename without quoting,
309 # * In Win: single quotes can be used in the filename without quoting,
310 # and we cannot use single quotes to quote the filename.
310 # and we cannot use single quotes to quote the filename.
311 # * In *nix: we can escape double quotes in a double quoted filename,
311 # * In *nix: we can escape double quotes in a double quoted filename,
312 # but can't escape single quotes in a single quoted filename.
312 # but can't escape single quotes in a single quoted filename.
313
313
314 # So to keep this code non-platform-specific and simple, we now only
314 # So to keep this code non-platform-specific and simple, we now only
315 # use double quotes to quote filenames, and escape when needed:
315 # use double quotes to quote filenames, and escape when needed:
316 if ' ' in path or "'" in path or '"' in path:
316 if ' ' in path or "'" in path or '"' in path:
317 path = '"%s"' % path.replace('"', '\\"')
317 path = '"%s"' % path.replace('"', '\\"')
318 self.execute('%%run %s' % path, hidden=hidden)
318 self.execute('%%run %s' % path, hidden=hidden)
319
319
320 #---------------------------------------------------------------------------
320 #---------------------------------------------------------------------------
321 # 'FrontendWidget' protected interface
321 # 'FrontendWidget' protected interface
322 #---------------------------------------------------------------------------
322 #---------------------------------------------------------------------------
323
323
324 def _complete(self):
324 def _complete(self):
325 """ Reimplemented to support IPython's improved completion machinery.
325 """ Reimplemented to support IPython's improved completion machinery.
326 """
326 """
327 # We let the kernel split the input line, so we *always* send an empty
327 # We let the kernel split the input line, so we *always* send an empty
328 # text field. Readline-based frontends do get a real text field which
328 # text field. Readline-based frontends do get a real text field which
329 # they can use.
329 # they can use.
330 text = ''
330 text = ''
331
331
332 # Send the completion request to the kernel
332 # Send the completion request to the kernel
333 msg_id = self.kernel_client.shell_channel.complete(
333 msg_id = self.kernel_client.shell_channel.complete(
334 text, # text
334 text, # text
335 self._get_input_buffer_cursor_line(), # line
335 self._get_input_buffer_cursor_line(), # line
336 self._get_input_buffer_cursor_column(), # cursor_pos
336 self._get_input_buffer_cursor_column(), # cursor_pos
337 self.input_buffer) # block
337 self.input_buffer) # block
338 pos = self._get_cursor().position()
338 pos = self._get_cursor().position()
339 info = self._CompletionRequest(msg_id, pos)
339 info = self._CompletionRequest(msg_id, pos)
340 self._request_info['complete'] = info
340 self._request_info['complete'] = info
341
341
342 def _process_execute_error(self, msg):
342 def _process_execute_error(self, msg):
343 """ Reimplemented for IPython-style traceback formatting.
343 """ Reimplemented for IPython-style traceback formatting.
344 """
344 """
345 content = msg['content']
345 content = msg['content']
346 traceback = '\n'.join(content['traceback']) + '\n'
346 traceback = '\n'.join(content['traceback']) + '\n'
347 if False:
347 if False:
348 # FIXME: For now, tracebacks come as plain text, so we can't use
348 # FIXME: For now, tracebacks come as plain text, so we can't use
349 # the html renderer yet. Once we refactor ultratb to produce
349 # the html renderer yet. Once we refactor ultratb to produce
350 # properly styled tracebacks, this branch should be the default
350 # properly styled tracebacks, this branch should be the default
351 traceback = traceback.replace(' ', '&nbsp;')
351 traceback = traceback.replace(' ', '&nbsp;')
352 traceback = traceback.replace('\n', '<br/>')
352 traceback = traceback.replace('\n', '<br/>')
353
353
354 ename = content['ename']
354 ename = content['ename']
355 ename_styled = '<span class="error">%s</span>' % ename
355 ename_styled = '<span class="error">%s</span>' % ename
356 traceback = traceback.replace(ename, ename_styled)
356 traceback = traceback.replace(ename, ename_styled)
357
357
358 self._append_html(traceback)
358 self._append_html(traceback)
359 else:
359 else:
360 # This is the fallback for now, using plain text with ansi escapes
360 # This is the fallback for now, using plain text with ansi escapes
361 self._append_plain_text(traceback)
361 self._append_plain_text(traceback)
362
362
363 def _process_execute_payload(self, item):
363 def _process_execute_payload(self, item):
364 """ Reimplemented to dispatch payloads to handler methods.
364 """ Reimplemented to dispatch payloads to handler methods.
365 """
365 """
366 handler = self._payload_handlers.get(item['source'])
366 handler = self._payload_handlers.get(item['source'])
367 if handler is None:
367 if handler is None:
368 # We have no handler for this type of payload, simply ignore it
368 # We have no handler for this type of payload, simply ignore it
369 return False
369 return False
370 else:
370 else:
371 handler(item)
371 handler(item)
372 return True
372 return True
373
373
374 def _show_interpreter_prompt(self, number=None):
374 def _show_interpreter_prompt(self, number=None):
375 """ Reimplemented for IPython-style prompts.
375 """ Reimplemented for IPython-style prompts.
376 """
376 """
377 # If a number was not specified, make a prompt number request.
377 # If a number was not specified, make a prompt number request.
378 if number is None:
378 if number is None:
379 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
379 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
380 info = self._ExecutionRequest(msg_id, 'prompt')
380 info = self._ExecutionRequest(msg_id, 'prompt')
381 self._request_info['execute'][msg_id] = info
381 self._request_info['execute'][msg_id] = info
382 return
382 return
383
383
384 # Show a new prompt and save information about it so that it can be
384 # Show a new prompt and save information about it so that it can be
385 # updated later if the prompt number turns out to be wrong.
385 # updated later if the prompt number turns out to be wrong.
386 self._prompt_sep = self.input_sep
386 self._prompt_sep = self.input_sep
387 self._show_prompt(self._make_in_prompt(number), html=True)
387 self._show_prompt(self._make_in_prompt(number), html=True)
388 block = self._control.document().lastBlock()
388 block = self._control.document().lastBlock()
389 length = len(self._prompt)
389 length = len(self._prompt)
390 self._previous_prompt_obj = self._PromptBlock(block, length, number)
390 self._previous_prompt_obj = self._PromptBlock(block, length, number)
391
391
392 # Update continuation prompt to reflect (possibly) new prompt length.
392 # Update continuation prompt to reflect (possibly) new prompt length.
393 self._set_continuation_prompt(
393 self._set_continuation_prompt(
394 self._make_continuation_prompt(self._prompt), html=True)
394 self._make_continuation_prompt(self._prompt), html=True)
395
395
396 def _show_interpreter_prompt_for_reply(self, msg):
396 def _show_interpreter_prompt_for_reply(self, msg):
397 """ Reimplemented for IPython-style prompts.
397 """ Reimplemented for IPython-style prompts.
398 """
398 """
399 # Update the old prompt number if necessary.
399 # Update the old prompt number if necessary.
400 content = msg['content']
400 content = msg['content']
401 # abort replies do not have any keys:
401 # abort replies do not have any keys:
402 if content['status'] == 'aborted':
402 if content['status'] == 'aborted':
403 if self._previous_prompt_obj:
403 if self._previous_prompt_obj:
404 previous_prompt_number = self._previous_prompt_obj.number
404 previous_prompt_number = self._previous_prompt_obj.number
405 else:
405 else:
406 previous_prompt_number = 0
406 previous_prompt_number = 0
407 else:
407 else:
408 previous_prompt_number = content['execution_count']
408 previous_prompt_number = content['execution_count']
409 if self._previous_prompt_obj and \
409 if self._previous_prompt_obj and \
410 self._previous_prompt_obj.number != previous_prompt_number:
410 self._previous_prompt_obj.number != previous_prompt_number:
411 block = self._previous_prompt_obj.block
411 block = self._previous_prompt_obj.block
412
412
413 # Make sure the prompt block has not been erased.
413 # Make sure the prompt block has not been erased.
414 if block.isValid() and block.text():
414 if block.isValid() and block.text():
415
415
416 # Remove the old prompt and insert a new prompt.
416 # Remove the old prompt and insert a new prompt.
417 cursor = QtGui.QTextCursor(block)
417 cursor = QtGui.QTextCursor(block)
418 cursor.movePosition(QtGui.QTextCursor.Right,
418 cursor.movePosition(QtGui.QTextCursor.Right,
419 QtGui.QTextCursor.KeepAnchor,
419 QtGui.QTextCursor.KeepAnchor,
420 self._previous_prompt_obj.length)
420 self._previous_prompt_obj.length)
421 prompt = self._make_in_prompt(previous_prompt_number)
421 prompt = self._make_in_prompt(previous_prompt_number)
422 self._prompt = self._insert_html_fetching_plain_text(
422 self._prompt = self._insert_html_fetching_plain_text(
423 cursor, prompt)
423 cursor, prompt)
424
424
425 # When the HTML is inserted, Qt blows away the syntax
425 # When the HTML is inserted, Qt blows away the syntax
426 # highlighting for the line, so we need to rehighlight it.
426 # highlighting for the line, so we need to rehighlight it.
427 self._highlighter.rehighlightBlock(cursor.block())
427 self._highlighter.rehighlightBlock(cursor.block())
428
428
429 self._previous_prompt_obj = None
429 self._previous_prompt_obj = None
430
430
431 # Show a new prompt with the kernel's estimated prompt number.
431 # Show a new prompt with the kernel's estimated prompt number.
432 self._show_interpreter_prompt(previous_prompt_number + 1)
432 self._show_interpreter_prompt(previous_prompt_number + 1)
433
433
434 #---------------------------------------------------------------------------
434 #---------------------------------------------------------------------------
435 # 'IPythonWidget' interface
435 # 'IPythonWidget' interface
436 #---------------------------------------------------------------------------
436 #---------------------------------------------------------------------------
437
437
438 def set_default_style(self, colors='lightbg'):
438 def set_default_style(self, colors='lightbg'):
439 """ Sets the widget style to the class defaults.
439 """ Sets the widget style to the class defaults.
440
440
441 Parameters:
441 Parameters:
442 -----------
442 -----------
443 colors : str, optional (default lightbg)
443 colors : str, optional (default lightbg)
444 Whether to use the default IPython light background or dark
444 Whether to use the default IPython light background or dark
445 background or B&W style.
445 background or B&W style.
446 """
446 """
447 colors = colors.lower()
447 colors = colors.lower()
448 if colors=='lightbg':
448 if colors=='lightbg':
449 self.style_sheet = styles.default_light_style_sheet
449 self.style_sheet = styles.default_light_style_sheet
450 self.syntax_style = styles.default_light_syntax_style
450 self.syntax_style = styles.default_light_syntax_style
451 elif colors=='linux':
451 elif colors=='linux':
452 self.style_sheet = styles.default_dark_style_sheet
452 self.style_sheet = styles.default_dark_style_sheet
453 self.syntax_style = styles.default_dark_syntax_style
453 self.syntax_style = styles.default_dark_syntax_style
454 elif colors=='nocolor':
454 elif colors=='nocolor':
455 self.style_sheet = styles.default_bw_style_sheet
455 self.style_sheet = styles.default_bw_style_sheet
456 self.syntax_style = styles.default_bw_syntax_style
456 self.syntax_style = styles.default_bw_syntax_style
457 else:
457 else:
458 raise KeyError("No such color scheme: %s"%colors)
458 raise KeyError("No such color scheme: %s"%colors)
459
459
460 #---------------------------------------------------------------------------
460 #---------------------------------------------------------------------------
461 # 'IPythonWidget' protected interface
461 # 'IPythonWidget' protected interface
462 #---------------------------------------------------------------------------
462 #---------------------------------------------------------------------------
463
463
464 def _edit(self, filename, line=None):
464 def _edit(self, filename, line=None):
465 """ Opens a Python script for editing.
465 """ Opens a Python script for editing.
466
466
467 Parameters:
467 Parameters:
468 -----------
468 -----------
469 filename : str
469 filename : str
470 A path to a local system file.
470 A path to a local system file.
471
471
472 line : int, optional
472 line : int, optional
473 A line of interest in the file.
473 A line of interest in the file.
474 """
474 """
475 if self.custom_edit:
475 if self.custom_edit:
476 self.custom_edit_requested.emit(filename, line)
476 self.custom_edit_requested.emit(filename, line)
477 elif not self.editor:
477 elif not self.editor:
478 self._append_plain_text('No default editor available.\n'
478 self._append_plain_text('No default editor available.\n'
479 'Specify a GUI text editor in the `IPythonWidget.editor` '
479 'Specify a GUI text editor in the `IPythonWidget.editor` '
480 'configurable to enable the %edit magic')
480 'configurable to enable the %edit magic')
481 else:
481 else:
482 try:
482 try:
483 filename = '"%s"' % filename
483 filename = '"%s"' % filename
484 if line and self.editor_line:
484 if line and self.editor_line:
485 command = self.editor_line.format(filename=filename,
485 command = self.editor_line.format(filename=filename,
486 line=line)
486 line=line)
487 else:
487 else:
488 try:
488 try:
489 command = self.editor.format()
489 command = self.editor.format()
490 except KeyError:
490 except KeyError:
491 command = self.editor.format(filename=filename)
491 command = self.editor.format(filename=filename)
492 else:
492 else:
493 command += ' ' + filename
493 command += ' ' + filename
494 except KeyError:
494 except KeyError:
495 self._append_plain_text('Invalid editor command.\n')
495 self._append_plain_text('Invalid editor command.\n')
496 else:
496 else:
497 try:
497 try:
498 Popen(command, shell=True)
498 Popen(command, shell=True)
499 except OSError:
499 except OSError:
500 msg = 'Opening editor with command "%s" failed.\n'
500 msg = 'Opening editor with command "%s" failed.\n'
501 self._append_plain_text(msg % command)
501 self._append_plain_text(msg % command)
502
502
503 def _make_in_prompt(self, number):
503 def _make_in_prompt(self, number):
504 """ Given a prompt number, returns an HTML In prompt.
504 """ Given a prompt number, returns an HTML In prompt.
505 """
505 """
506 try:
506 try:
507 body = self.in_prompt % number
507 body = self.in_prompt % number
508 except TypeError:
508 except TypeError:
509 # allow in_prompt to leave out number, e.g. '>>> '
509 # allow in_prompt to leave out number, e.g. '>>> '
510 body = self.in_prompt
510 body = self.in_prompt
511 return '<span class="in-prompt">%s</span>' % body
511 return '<span class="in-prompt">%s</span>' % body
512
512
513 def _make_continuation_prompt(self, prompt):
513 def _make_continuation_prompt(self, prompt):
514 """ Given a plain text version of an In prompt, returns an HTML
514 """ Given a plain text version of an In prompt, returns an HTML
515 continuation prompt.
515 continuation prompt.
516 """
516 """
517 end_chars = '...: '
517 end_chars = '...: '
518 space_count = len(prompt.lstrip('\n')) - len(end_chars)
518 space_count = len(prompt.lstrip('\n')) - len(end_chars)
519 body = '&nbsp;' * space_count + end_chars
519 body = '&nbsp;' * space_count + end_chars
520 return '<span class="in-prompt">%s</span>' % body
520 return '<span class="in-prompt">%s</span>' % body
521
521
522 def _make_out_prompt(self, number):
522 def _make_out_prompt(self, number):
523 """ Given a prompt number, returns an HTML Out prompt.
523 """ Given a prompt number, returns an HTML Out prompt.
524 """
524 """
525 body = self.out_prompt % number
525 body = self.out_prompt % number
526 return '<span class="out-prompt">%s</span>' % body
526 return '<span class="out-prompt">%s</span>' % body
527
527
528 #------ Payload handlers --------------------------------------------------
528 #------ Payload handlers --------------------------------------------------
529
529
530 # Payload handlers with a generic interface: each takes the opaque payload
530 # Payload handlers with a generic interface: each takes the opaque payload
531 # dict, unpacks it and calls the underlying functions with the necessary
531 # dict, unpacks it and calls the underlying functions with the necessary
532 # arguments.
532 # arguments.
533
533
534 def _handle_payload_edit(self, item):
534 def _handle_payload_edit(self, item):
535 self._edit(item['filename'], item['line_number'])
535 self._edit(item['filename'], item['line_number'])
536
536
537 def _handle_payload_exit(self, item):
537 def _handle_payload_exit(self, item):
538 self._keep_kernel_on_exit = item['keepkernel']
538 self._keep_kernel_on_exit = item['keepkernel']
539 self.exit_requested.emit(self)
539 self.exit_requested.emit(self)
540
540
541 def _handle_payload_next_input(self, item):
541 def _handle_payload_next_input(self, item):
542 self.input_buffer = dedent(item['text'].rstrip())
542 self.input_buffer = dedent(item['text'].rstrip())
543
543
544 def _handle_payload_page(self, item):
544 def _handle_payload_page(self, item):
545 # Since the plain text widget supports only a very small subset of HTML
545 # Since the plain text widget supports only a very small subset of HTML
546 # and we have no control over the HTML source, we only page HTML
546 # and we have no control over the HTML source, we only page HTML
547 # payloads in the rich text widget.
547 # payloads in the rich text widget.
548 if item['html'] and self.kind == 'rich':
548 if item['html'] and self.kind == 'rich':
549 self._page(item['html'], html=True)
549 self._page(item['html'], html=True)
550 else:
550 else:
551 self._page(item['text'], html=False)
551 self._page(item['text'], html=False)
552
552
553 #------ Trait change handlers --------------------------------------------
553 #------ Trait change handlers --------------------------------------------
554
554
555 def _style_sheet_changed(self):
555 def _style_sheet_changed(self):
556 """ Set the style sheets of the underlying widgets.
556 """ Set the style sheets of the underlying widgets.
557 """
557 """
558 self.setStyleSheet(self.style_sheet)
558 self.setStyleSheet(self.style_sheet)
559 if self._control is not None:
559 if self._control is not None:
560 self._control.document().setDefaultStyleSheet(self.style_sheet)
560 self._control.document().setDefaultStyleSheet(self.style_sheet)
561 bg_color = self._control.palette().window().color()
561 bg_color = self._control.palette().window().color()
562 self._ansi_processor.set_background_color(bg_color)
562 self._ansi_processor.set_background_color(bg_color)
563
563
564 if self._page_control is not None:
564 if self._page_control is not None:
565 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
565 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
566
566
567
567
568
568
569 def _syntax_style_changed(self):
569 def _syntax_style_changed(self):
570 """ Set the style for the syntax highlighter.
570 """ Set the style for the syntax highlighter.
571 """
571 """
572 if self._highlighter is None:
572 if self._highlighter is None:
573 # ignore premature calls
573 # ignore premature calls
574 return
574 return
575 if self.syntax_style:
575 if self.syntax_style:
576 self._highlighter.set_style(self.syntax_style)
576 self._highlighter.set_style(self.syntax_style)
577 else:
577 else:
578 self._highlighter.set_style_sheet(self.style_sheet)
578 self._highlighter.set_style_sheet(self.style_sheet)
579
579
580 #------ Trait default initializers -----------------------------------------
580 #------ Trait default initializers -----------------------------------------
581
581
582 def _banner_default(self):
582 def _banner_default(self):
583 from IPython.core.usage import default_gui_banner
583 from IPython.core.usage import default_gui_banner
584 return default_gui_banner
584 return default_gui_banner
@@ -1,341 +1,341 b''
1 #-----------------------------------------------------------------------------
1 #-----------------------------------------------------------------------------
2 # Copyright (c) 2010, IPython Development Team.
2 # Copyright (c) 2010, IPython Development Team.
3 #
3 #
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5 #
5 #
6 # The full license is in the file COPYING.txt, distributed with this software.
6 # The full license is in the file COPYING.txt, distributed with this software.
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard libary imports.
9 # Standard libary imports.
10 from base64 import decodestring
10 from base64 import decodestring
11 import os
11 import os
12 import re
12 import re
13
13
14 # System libary imports.
14 # System libary imports.
15 from IPython.external.qt import QtCore, QtGui
15 from IPython.external.qt import QtCore, QtGui
16
16
17 # Local imports
17 # Local imports
18 from IPython.utils.traitlets import Bool
18 from IPython.utils.traitlets import Bool
19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
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 class RichIPythonWidget(IPythonWidget):
23 class RichIPythonWidget(IPythonWidget):
24 """ An IPythonWidget that supports rich text, including lists, images, and
24 """ An IPythonWidget that supports rich text, including lists, images, and
25 tables. Note that raw performance will be reduced compared to the plain
25 tables. Note that raw performance will be reduced compared to the plain
26 text version.
26 text version.
27 """
27 """
28
28
29 # RichIPythonWidget protected class variables.
29 # RichIPythonWidget protected class variables.
30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
31 _jpg_supported = Bool(False)
31 _jpg_supported = Bool(False)
32
32
33 # Used to determine whether a given html export attempt has already
33 # Used to determine whether a given html export attempt has already
34 # displayed a warning about being unable to convert a png to svg.
34 # displayed a warning about being unable to convert a png to svg.
35 _svg_warning_displayed = False
35 _svg_warning_displayed = False
36
36
37 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
38 # 'object' interface
38 # 'object' interface
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40
40
41 def __init__(self, *args, **kw):
41 def __init__(self, *args, **kw):
42 """ Create a RichIPythonWidget.
42 """ Create a RichIPythonWidget.
43 """
43 """
44 kw['kind'] = 'rich'
44 kw['kind'] = 'rich'
45 super(RichIPythonWidget, self).__init__(*args, **kw)
45 super(RichIPythonWidget, self).__init__(*args, **kw)
46
46
47 # Configure the ConsoleWidget HTML exporter for our formats.
47 # Configure the ConsoleWidget HTML exporter for our formats.
48 self._html_exporter.image_tag = self._get_image_tag
48 self._html_exporter.image_tag = self._get_image_tag
49
49
50 # Dictionary for resolving document resource names to SVG data.
50 # Dictionary for resolving document resource names to SVG data.
51 self._name_to_svg_map = {}
51 self._name_to_svg_map = {}
52
52
53 # Do we support jpg ?
53 # Do we support jpg ?
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
55 # it is not always supported.
55 # it is not always supported.
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
57 self._jpg_supported = 'jpeg' in _supported_format
57 self._jpg_supported = 'jpeg' in _supported_format
58
58
59
59
60 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
61 # 'ConsoleWidget' public interface overides
61 # 'ConsoleWidget' public interface overides
62 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
63
63
64 def export_html(self):
64 def export_html(self):
65 """ Shows a dialog to export HTML/XML in various formats.
65 """ Shows a dialog to export HTML/XML in various formats.
66
66
67 Overridden in order to reset the _svg_warning_displayed flag prior
67 Overridden in order to reset the _svg_warning_displayed flag prior
68 to the export running.
68 to the export running.
69 """
69 """
70 self._svg_warning_displayed = False
70 self._svg_warning_displayed = False
71 super(RichIPythonWidget, self).export_html()
71 super(RichIPythonWidget, self).export_html()
72
72
73
73
74 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
75 # 'ConsoleWidget' protected interface
75 # 'ConsoleWidget' protected interface
76 #---------------------------------------------------------------------------
76 #---------------------------------------------------------------------------
77
77
78 def _context_menu_make(self, pos):
78 def _context_menu_make(self, pos):
79 """ Reimplemented to return a custom context menu for images.
79 """ Reimplemented to return a custom context menu for images.
80 """
80 """
81 format = self._control.cursorForPosition(pos).charFormat()
81 format = self._control.cursorForPosition(pos).charFormat()
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
83 if name:
83 if name:
84 menu = QtGui.QMenu()
84 menu = QtGui.QMenu()
85
85
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
88 menu.addSeparator()
88 menu.addSeparator()
89
89
90 svg = self._name_to_svg_map.get(name, None)
90 svg = self._name_to_svg_map.get(name, None)
91 if svg is not None:
91 if svg is not None:
92 menu.addSeparator()
92 menu.addSeparator()
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
94 menu.addAction('Save SVG As...',
94 menu.addAction('Save SVG As...',
95 lambda: save_svg(svg, self._control))
95 lambda: save_svg(svg, self._control))
96 else:
96 else:
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
98 return menu
98 return menu
99
99
100 #---------------------------------------------------------------------------
100 #---------------------------------------------------------------------------
101 # 'BaseFrontendMixin' abstract interface
101 # 'BaseFrontendMixin' abstract interface
102 #---------------------------------------------------------------------------
102 #---------------------------------------------------------------------------
103 def _pre_image_append(self, msg, prompt_number):
103 def _pre_image_append(self, msg, prompt_number):
104 """ Append the Out[] prompt and make the output nicer
104 """ Append the Out[] prompt and make the output nicer
105
105
106 Shared code for some the following if statement
106 Shared code for some the following if statement
107 """
107 """
108 self.log.debug("pyout: %s", msg.get('content', ''))
108 self.log.debug("pyout: %s", msg.get('content', ''))
109 self._append_plain_text(self.output_sep, True)
109 self._append_plain_text(self.output_sep, True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
111 self._append_plain_text('\n', True)
111 self._append_plain_text('\n', True)
112
112
113 def _handle_pyout(self, msg):
113 def _handle_pyout(self, msg):
114 """ Overridden to handle rich data types, like SVG.
114 """ Overridden to handle rich data types, like SVG.
115 """
115 """
116 if not self._hidden and self._is_from_this_session(msg):
116 if not self._hidden and self._is_from_this_session(msg):
117 content = msg['content']
117 content = msg['content']
118 prompt_number = content.get('execution_count', 0)
118 prompt_number = content.get('execution_count', 0)
119 data = content['data']
119 data = content['data']
120 metadata = msg['content']['metadata']
120 metadata = msg['content']['metadata']
121 if 'image/svg+xml' in data:
121 if 'image/svg+xml' in data:
122 self._pre_image_append(msg, prompt_number)
122 self._pre_image_append(msg, prompt_number)
123 self._append_svg(data['image/svg+xml'], True)
123 self._append_svg(data['image/svg+xml'], True)
124 self._append_html(self.output_sep2, True)
124 self._append_html(self.output_sep2, True)
125 elif 'image/png' in data:
125 elif 'image/png' in data:
126 self._pre_image_append(msg, prompt_number)
126 self._pre_image_append(msg, prompt_number)
127 png = decodestring(data['image/png'].encode('ascii'))
127 png = decodestring(data['image/png'].encode('ascii'))
128 self._append_png(png, True, metadata=metadata.get('image/png', None))
128 self._append_png(png, True, metadata=metadata.get('image/png', None))
129 self._append_html(self.output_sep2, True)
129 self._append_html(self.output_sep2, True)
130 elif 'image/jpeg' in data and self._jpg_supported:
130 elif 'image/jpeg' in data and self._jpg_supported:
131 self._pre_image_append(msg, prompt_number)
131 self._pre_image_append(msg, prompt_number)
132 jpg = decodestring(data['image/jpeg'].encode('ascii'))
132 jpg = decodestring(data['image/jpeg'].encode('ascii'))
133 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
133 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
134 self._append_html(self.output_sep2, True)
134 self._append_html(self.output_sep2, True)
135 else:
135 else:
136 # Default back to the plain text representation.
136 # Default back to the plain text representation.
137 return super(RichIPythonWidget, self)._handle_pyout(msg)
137 return super(RichIPythonWidget, self)._handle_pyout(msg)
138
138
139 def _handle_display_data(self, msg):
139 def _handle_display_data(self, msg):
140 """ Overridden to handle rich data types, like SVG.
140 """ Overridden to handle rich data types, like SVG.
141 """
141 """
142 if not self._hidden and self._is_from_this_session(msg):
142 if not self._hidden and self._is_from_this_session(msg):
143 source = msg['content']['source']
143 source = msg['content']['source']
144 data = msg['content']['data']
144 data = msg['content']['data']
145 metadata = msg['content']['metadata']
145 metadata = msg['content']['metadata']
146 # Try to use the svg or html representations.
146 # Try to use the svg or html representations.
147 # FIXME: Is this the right ordering of things to try?
147 # FIXME: Is this the right ordering of things to try?
148 if 'image/svg+xml' in data:
148 if 'image/svg+xml' in data:
149 self.log.debug("display: %s", msg.get('content', ''))
149 self.log.debug("display: %s", msg.get('content', ''))
150 svg = data['image/svg+xml']
150 svg = data['image/svg+xml']
151 self._append_svg(svg, True)
151 self._append_svg(svg, True)
152 elif 'image/png' in data:
152 elif 'image/png' in data:
153 self.log.debug("display: %s", msg.get('content', ''))
153 self.log.debug("display: %s", msg.get('content', ''))
154 # PNG data is base64 encoded as it passes over the network
154 # PNG data is base64 encoded as it passes over the network
155 # in a JSON structure so we decode it.
155 # in a JSON structure so we decode it.
156 png = decodestring(data['image/png'].encode('ascii'))
156 png = decodestring(data['image/png'].encode('ascii'))
157 self._append_png(png, True, metadata=metadata.get('image/png', None))
157 self._append_png(png, True, metadata=metadata.get('image/png', None))
158 elif 'image/jpeg' in data and self._jpg_supported:
158 elif 'image/jpeg' in data and self._jpg_supported:
159 self.log.debug("display: %s", msg.get('content', ''))
159 self.log.debug("display: %s", msg.get('content', ''))
160 jpg = decodestring(data['image/jpeg'].encode('ascii'))
160 jpg = decodestring(data['image/jpeg'].encode('ascii'))
161 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
161 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
162 else:
162 else:
163 # Default back to the plain text representation.
163 # Default back to the plain text representation.
164 return super(RichIPythonWidget, self)._handle_display_data(msg)
164 return super(RichIPythonWidget, self)._handle_display_data(msg)
165
165
166 #---------------------------------------------------------------------------
166 #---------------------------------------------------------------------------
167 # 'RichIPythonWidget' protected interface
167 # 'RichIPythonWidget' protected interface
168 #---------------------------------------------------------------------------
168 #---------------------------------------------------------------------------
169
169
170 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
170 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
171 """ Append raw JPG data to the widget."""
171 """ Append raw JPG data to the widget."""
172 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
172 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
173
173
174 def _append_png(self, png, before_prompt=False, metadata=None):
174 def _append_png(self, png, before_prompt=False, metadata=None):
175 """ Append raw PNG data to the widget.
175 """ Append raw PNG data to the widget.
176 """
176 """
177 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
177 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
178
178
179 def _append_svg(self, svg, before_prompt=False):
179 def _append_svg(self, svg, before_prompt=False):
180 """ Append raw SVG data to the widget.
180 """ Append raw SVG data to the widget.
181 """
181 """
182 self._append_custom(self._insert_svg, svg, before_prompt)
182 self._append_custom(self._insert_svg, svg, before_prompt)
183
183
184 def _add_image(self, image):
184 def _add_image(self, image):
185 """ Adds the specified QImage to the document and returns a
185 """ Adds the specified QImage to the document and returns a
186 QTextImageFormat that references it.
186 QTextImageFormat that references it.
187 """
187 """
188 document = self._control.document()
188 document = self._control.document()
189 name = str(image.cacheKey())
189 name = str(image.cacheKey())
190 document.addResource(QtGui.QTextDocument.ImageResource,
190 document.addResource(QtGui.QTextDocument.ImageResource,
191 QtCore.QUrl(name), image)
191 QtCore.QUrl(name), image)
192 format = QtGui.QTextImageFormat()
192 format = QtGui.QTextImageFormat()
193 format.setName(name)
193 format.setName(name)
194 return format
194 return format
195
195
196 def _copy_image(self, name):
196 def _copy_image(self, name):
197 """ Copies the ImageResource with 'name' to the clipboard.
197 """ Copies the ImageResource with 'name' to the clipboard.
198 """
198 """
199 image = self._get_image(name)
199 image = self._get_image(name)
200 QtGui.QApplication.clipboard().setImage(image)
200 QtGui.QApplication.clipboard().setImage(image)
201
201
202 def _get_image(self, name):
202 def _get_image(self, name):
203 """ Returns the QImage stored as the ImageResource with 'name'.
203 """ Returns the QImage stored as the ImageResource with 'name'.
204 """
204 """
205 document = self._control.document()
205 document = self._control.document()
206 image = document.resource(QtGui.QTextDocument.ImageResource,
206 image = document.resource(QtGui.QTextDocument.ImageResource,
207 QtCore.QUrl(name))
207 QtCore.QUrl(name))
208 return image
208 return image
209
209
210 def _get_image_tag(self, match, path = None, format = "png"):
210 def _get_image_tag(self, match, path = None, format = "png"):
211 """ Return (X)HTML mark-up for the image-tag given by match.
211 """ Return (X)HTML mark-up for the image-tag given by match.
212
212
213 Parameters
213 Parameters
214 ----------
214 ----------
215 match : re.SRE_Match
215 match : re.SRE_Match
216 A match to an HTML image tag as exported by Qt, with
216 A match to an HTML image tag as exported by Qt, with
217 match.group("Name") containing the matched image ID.
217 match.group("Name") containing the matched image ID.
218
218
219 path : string|None, optional [default None]
219 path : string|None, optional [default None]
220 If not None, specifies a path to which supporting files may be
220 If not None, specifies a path to which supporting files may be
221 written (e.g., for linked images). If None, all images are to be
221 written (e.g., for linked images). If None, all images are to be
222 included inline.
222 included inline.
223
223
224 format : "png"|"svg"|"jpg", optional [default "png"]
224 format : "png"|"svg"|"jpg", optional [default "png"]
225 Format for returned or referenced images.
225 Format for returned or referenced images.
226 """
226 """
227 if format in ("png","jpg"):
227 if format in ("png","jpg"):
228 try:
228 try:
229 image = self._get_image(match.group("name"))
229 image = self._get_image(match.group("name"))
230 except KeyError:
230 except KeyError:
231 return "<b>Couldn't find image %s</b>" % match.group("name")
231 return "<b>Couldn't find image %s</b>" % match.group("name")
232
232
233 if path is not None:
233 if path is not None:
234 if not os.path.exists(path):
234 if not os.path.exists(path):
235 os.mkdir(path)
235 os.mkdir(path)
236 relpath = os.path.basename(path)
236 relpath = os.path.basename(path)
237 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
237 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
238 "PNG"):
238 "PNG"):
239 return '<img src="%s/qt_img%s.%s">' % (relpath,
239 return '<img src="%s/qt_img%s.%s">' % (relpath,
240 match.group("name"),format)
240 match.group("name"),format)
241 else:
241 else:
242 return "<b>Couldn't save image!</b>"
242 return "<b>Couldn't save image!</b>"
243 else:
243 else:
244 ba = QtCore.QByteArray()
244 ba = QtCore.QByteArray()
245 buffer_ = QtCore.QBuffer(ba)
245 buffer_ = QtCore.QBuffer(ba)
246 buffer_.open(QtCore.QIODevice.WriteOnly)
246 buffer_.open(QtCore.QIODevice.WriteOnly)
247 image.save(buffer_, format.upper())
247 image.save(buffer_, format.upper())
248 buffer_.close()
248 buffer_.close()
249 return '<img src="data:image/%s;base64,\n%s\n" />' % (
249 return '<img src="data:image/%s;base64,\n%s\n" />' % (
250 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
250 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
251
251
252 elif format == "svg":
252 elif format == "svg":
253 try:
253 try:
254 svg = str(self._name_to_svg_map[match.group("name")])
254 svg = str(self._name_to_svg_map[match.group("name")])
255 except KeyError:
255 except KeyError:
256 if not self._svg_warning_displayed:
256 if not self._svg_warning_displayed:
257 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
257 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
258 'Cannot convert PNG images to SVG, export with PNG figures instead. '
258 'Cannot convert PNG images to SVG, export with PNG figures instead. '
259 'If you want to export matplotlib figures as SVG, add '
259 'If you want to export matplotlib figures as SVG, add '
260 'to your ipython config:\n\n'
260 'to your ipython config:\n\n'
261 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
261 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
262 'And regenerate the figures.',
262 'And regenerate the figures.',
263 QtGui.QMessageBox.Ok)
263 QtGui.QMessageBox.Ok)
264 self._svg_warning_displayed = True
264 self._svg_warning_displayed = True
265 return ("<b>Cannot convert PNG images to SVG.</b> "
265 return ("<b>Cannot convert PNG images to SVG.</b> "
266 "You must export this session with PNG images. "
266 "You must export this session with PNG images. "
267 "If you want to export matplotlib figures as SVG, add to your config "
267 "If you want to export matplotlib figures as SVG, add to your config "
268 "<span>c.InlineBackend.figure_format = 'svg'</span> "
268 "<span>c.InlineBackend.figure_format = 'svg'</span> "
269 "and regenerate the figures.")
269 "and regenerate the figures.")
270
270
271 # Not currently checking path, because it's tricky to find a
271 # Not currently checking path, because it's tricky to find a
272 # cross-browser way to embed external SVG images (e.g., via
272 # cross-browser way to embed external SVG images (e.g., via
273 # object or embed tags).
273 # object or embed tags).
274
274
275 # Chop stand-alone header from matplotlib SVG
275 # Chop stand-alone header from matplotlib SVG
276 offset = svg.find("<svg")
276 offset = svg.find("<svg")
277 assert(offset > -1)
277 assert(offset > -1)
278
278
279 return svg[offset:]
279 return svg[offset:]
280
280
281 else:
281 else:
282 return '<b>Unrecognized image format</b>'
282 return '<b>Unrecognized image format</b>'
283
283
284 def _insert_jpg(self, cursor, jpg, metadata=None):
284 def _insert_jpg(self, cursor, jpg, metadata=None):
285 """ Insert raw PNG data into the widget."""
285 """ Insert raw PNG data into the widget."""
286 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
286 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
287
287
288 def _insert_png(self, cursor, png, metadata=None):
288 def _insert_png(self, cursor, png, metadata=None):
289 """ Insert raw PNG data into the widget.
289 """ Insert raw PNG data into the widget.
290 """
290 """
291 self._insert_img(cursor, png, 'png', metadata=metadata)
291 self._insert_img(cursor, png, 'png', metadata=metadata)
292
292
293 def _insert_img(self, cursor, img, fmt, metadata=None):
293 def _insert_img(self, cursor, img, fmt, metadata=None):
294 """ insert a raw image, jpg or png """
294 """ insert a raw image, jpg or png """
295 if metadata:
295 if metadata:
296 width = metadata.get('width', None)
296 width = metadata.get('width', None)
297 height = metadata.get('height', None)
297 height = metadata.get('height', None)
298 else:
298 else:
299 width = height = None
299 width = height = None
300 try:
300 try:
301 image = QtGui.QImage()
301 image = QtGui.QImage()
302 image.loadFromData(img, fmt.upper())
302 image.loadFromData(img, fmt.upper())
303 if width and height:
303 if width and height:
304 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
304 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
305 elif width and not height:
305 elif width and not height:
306 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
306 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
307 elif height and not width:
307 elif height and not width:
308 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
308 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
309 except ValueError:
309 except ValueError:
310 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
310 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
311 else:
311 else:
312 format = self._add_image(image)
312 format = self._add_image(image)
313 cursor.insertBlock()
313 cursor.insertBlock()
314 cursor.insertImage(format)
314 cursor.insertImage(format)
315 cursor.insertBlock()
315 cursor.insertBlock()
316
316
317 def _insert_svg(self, cursor, svg):
317 def _insert_svg(self, cursor, svg):
318 """ Insert raw SVG data into the widet.
318 """ Insert raw SVG data into the widet.
319 """
319 """
320 try:
320 try:
321 image = svg_to_image(svg)
321 image = svg_to_image(svg)
322 except ValueError:
322 except ValueError:
323 self._insert_plain_text(cursor, 'Received invalid SVG data.')
323 self._insert_plain_text(cursor, 'Received invalid SVG data.')
324 else:
324 else:
325 format = self._add_image(image)
325 format = self._add_image(image)
326 self._name_to_svg_map[format.name()] = svg
326 self._name_to_svg_map[format.name()] = svg
327 cursor.insertBlock()
327 cursor.insertBlock()
328 cursor.insertImage(format)
328 cursor.insertImage(format)
329 cursor.insertBlock()
329 cursor.insertBlock()
330
330
331 def _save_image(self, name, format='PNG'):
331 def _save_image(self, name, format='PNG'):
332 """ Shows a save dialog for the ImageResource with 'name'.
332 """ Shows a save dialog for the ImageResource with 'name'.
333 """
333 """
334 dialog = QtGui.QFileDialog(self._control, 'Save Image')
334 dialog = QtGui.QFileDialog(self._control, 'Save Image')
335 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
335 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
336 dialog.setDefaultSuffix(format.lower())
336 dialog.setDefaultSuffix(format.lower())
337 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
337 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
338 if dialog.exec_():
338 if dialog.exec_():
339 filename = dialog.selectedFiles()[0]
339 filename = dialog.selectedFiles()[0]
340 image = self._get_image(name)
340 image = self._get_image(name)
341 image.save(filename, format)
341 image.save(filename, format)
@@ -1,220 +1,220 b''
1 """ Defines a KernelManager that provides signals and slots.
1 """ Defines a KernelManager that provides signals and slots.
2 """
2 """
3
3
4 # System library imports.
4 # System library imports.
5 from IPython.external.qt import QtCore
5 from IPython.external.qt import QtCore
6
6
7 # IPython imports.
7 # IPython imports.
8 from IPython.utils.traitlets import HasTraits, Type
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 class ChannelQObject(SuperQObject):
12 class ChannelQObject(SuperQObject):
13
13
14 # Emitted when the channel is started.
14 # Emitted when the channel is started.
15 started = QtCore.Signal()
15 started = QtCore.Signal()
16
16
17 # Emitted when the channel is stopped.
17 # Emitted when the channel is stopped.
18 stopped = QtCore.Signal()
18 stopped = QtCore.Signal()
19
19
20 #---------------------------------------------------------------------------
20 #---------------------------------------------------------------------------
21 # Channel interface
21 # Channel interface
22 #---------------------------------------------------------------------------
22 #---------------------------------------------------------------------------
23
23
24 def start(self):
24 def start(self):
25 """ Reimplemented to emit signal.
25 """ Reimplemented to emit signal.
26 """
26 """
27 super(ChannelQObject, self).start()
27 super(ChannelQObject, self).start()
28 self.started.emit()
28 self.started.emit()
29
29
30 def stop(self):
30 def stop(self):
31 """ Reimplemented to emit signal.
31 """ Reimplemented to emit signal.
32 """
32 """
33 super(ChannelQObject, self).stop()
33 super(ChannelQObject, self).stop()
34 self.stopped.emit()
34 self.stopped.emit()
35
35
36 #---------------------------------------------------------------------------
36 #---------------------------------------------------------------------------
37 # InProcessChannel interface
37 # InProcessChannel interface
38 #---------------------------------------------------------------------------
38 #---------------------------------------------------------------------------
39
39
40 def call_handlers_later(self, *args, **kwds):
40 def call_handlers_later(self, *args, **kwds):
41 """ Call the message handlers later.
41 """ Call the message handlers later.
42 """
42 """
43 do_later = lambda: self.call_handlers(*args, **kwds)
43 do_later = lambda: self.call_handlers(*args, **kwds)
44 QtCore.QTimer.singleShot(0, do_later)
44 QtCore.QTimer.singleShot(0, do_later)
45
45
46 def process_events(self):
46 def process_events(self):
47 """ Process any pending GUI events.
47 """ Process any pending GUI events.
48 """
48 """
49 QtCore.QCoreApplication.instance().processEvents()
49 QtCore.QCoreApplication.instance().processEvents()
50
50
51
51
52 class QtShellChannelMixin(ChannelQObject):
52 class QtShellChannelMixin(ChannelQObject):
53
53
54 # Emitted when any message is received.
54 # Emitted when any message is received.
55 message_received = QtCore.Signal(object)
55 message_received = QtCore.Signal(object)
56
56
57 # Emitted when a reply has been received for the corresponding request type.
57 # Emitted when a reply has been received for the corresponding request type.
58 execute_reply = QtCore.Signal(object)
58 execute_reply = QtCore.Signal(object)
59 complete_reply = QtCore.Signal(object)
59 complete_reply = QtCore.Signal(object)
60 object_info_reply = QtCore.Signal(object)
60 object_info_reply = QtCore.Signal(object)
61 history_reply = QtCore.Signal(object)
61 history_reply = QtCore.Signal(object)
62
62
63 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
64 # 'ShellChannel' interface
64 # 'ShellChannel' interface
65 #---------------------------------------------------------------------------
65 #---------------------------------------------------------------------------
66
66
67 def call_handlers(self, msg):
67 def call_handlers(self, msg):
68 """ Reimplemented to emit signals instead of making callbacks.
68 """ Reimplemented to emit signals instead of making callbacks.
69 """
69 """
70 # Emit the generic signal.
70 # Emit the generic signal.
71 self.message_received.emit(msg)
71 self.message_received.emit(msg)
72
72
73 # Emit signals for specialized message types.
73 # Emit signals for specialized message types.
74 msg_type = msg['header']['msg_type']
74 msg_type = msg['header']['msg_type']
75 signal = getattr(self, msg_type, None)
75 signal = getattr(self, msg_type, None)
76 if signal:
76 if signal:
77 signal.emit(msg)
77 signal.emit(msg)
78
78
79
79
80 class QtIOPubChannelMixin(ChannelQObject):
80 class QtIOPubChannelMixin(ChannelQObject):
81
81
82 # Emitted when any message is received.
82 # Emitted when any message is received.
83 message_received = QtCore.Signal(object)
83 message_received = QtCore.Signal(object)
84
84
85 # Emitted when a message of type 'stream' is received.
85 # Emitted when a message of type 'stream' is received.
86 stream_received = QtCore.Signal(object)
86 stream_received = QtCore.Signal(object)
87
87
88 # Emitted when a message of type 'pyin' is received.
88 # Emitted when a message of type 'pyin' is received.
89 pyin_received = QtCore.Signal(object)
89 pyin_received = QtCore.Signal(object)
90
90
91 # Emitted when a message of type 'pyout' is received.
91 # Emitted when a message of type 'pyout' is received.
92 pyout_received = QtCore.Signal(object)
92 pyout_received = QtCore.Signal(object)
93
93
94 # Emitted when a message of type 'pyerr' is received.
94 # Emitted when a message of type 'pyerr' is received.
95 pyerr_received = QtCore.Signal(object)
95 pyerr_received = QtCore.Signal(object)
96
96
97 # Emitted when a message of type 'display_data' is received
97 # Emitted when a message of type 'display_data' is received
98 display_data_received = QtCore.Signal(object)
98 display_data_received = QtCore.Signal(object)
99
99
100 # Emitted when a crash report message is received from the kernel's
100 # Emitted when a crash report message is received from the kernel's
101 # last-resort sys.excepthook.
101 # last-resort sys.excepthook.
102 crash_received = QtCore.Signal(object)
102 crash_received = QtCore.Signal(object)
103
103
104 # Emitted when a shutdown is noticed.
104 # Emitted when a shutdown is noticed.
105 shutdown_reply_received = QtCore.Signal(object)
105 shutdown_reply_received = QtCore.Signal(object)
106
106
107 #---------------------------------------------------------------------------
107 #---------------------------------------------------------------------------
108 # 'IOPubChannel' interface
108 # 'IOPubChannel' interface
109 #---------------------------------------------------------------------------
109 #---------------------------------------------------------------------------
110
110
111 def call_handlers(self, msg):
111 def call_handlers(self, msg):
112 """ Reimplemented to emit signals instead of making callbacks.
112 """ Reimplemented to emit signals instead of making callbacks.
113 """
113 """
114 # Emit the generic signal.
114 # Emit the generic signal.
115 self.message_received.emit(msg)
115 self.message_received.emit(msg)
116 # Emit signals for specialized message types.
116 # Emit signals for specialized message types.
117 msg_type = msg['header']['msg_type']
117 msg_type = msg['header']['msg_type']
118 signal = getattr(self, msg_type + '_received', None)
118 signal = getattr(self, msg_type + '_received', None)
119 if signal:
119 if signal:
120 signal.emit(msg)
120 signal.emit(msg)
121 elif msg_type in ('stdout', 'stderr'):
121 elif msg_type in ('stdout', 'stderr'):
122 self.stream_received.emit(msg)
122 self.stream_received.emit(msg)
123
123
124 def flush(self):
124 def flush(self):
125 """ Reimplemented to ensure that signals are dispatched immediately.
125 """ Reimplemented to ensure that signals are dispatched immediately.
126 """
126 """
127 super(QtIOPubChannelMixin, self).flush()
127 super(QtIOPubChannelMixin, self).flush()
128 QtCore.QCoreApplication.instance().processEvents()
128 QtCore.QCoreApplication.instance().processEvents()
129
129
130
130
131 class QtStdInChannelMixin(ChannelQObject):
131 class QtStdInChannelMixin(ChannelQObject):
132
132
133 # Emitted when any message is received.
133 # Emitted when any message is received.
134 message_received = QtCore.Signal(object)
134 message_received = QtCore.Signal(object)
135
135
136 # Emitted when an input request is received.
136 # Emitted when an input request is received.
137 input_requested = QtCore.Signal(object)
137 input_requested = QtCore.Signal(object)
138
138
139 #---------------------------------------------------------------------------
139 #---------------------------------------------------------------------------
140 # 'StdInChannel' interface
140 # 'StdInChannel' interface
141 #---------------------------------------------------------------------------
141 #---------------------------------------------------------------------------
142
142
143 def call_handlers(self, msg):
143 def call_handlers(self, msg):
144 """ Reimplemented to emit signals instead of making callbacks.
144 """ Reimplemented to emit signals instead of making callbacks.
145 """
145 """
146 # Emit the generic signal.
146 # Emit the generic signal.
147 self.message_received.emit(msg)
147 self.message_received.emit(msg)
148
148
149 # Emit signals for specialized message types.
149 # Emit signals for specialized message types.
150 msg_type = msg['header']['msg_type']
150 msg_type = msg['header']['msg_type']
151 if msg_type == 'input_request':
151 if msg_type == 'input_request':
152 self.input_requested.emit(msg)
152 self.input_requested.emit(msg)
153
153
154
154
155 class QtHBChannelMixin(ChannelQObject):
155 class QtHBChannelMixin(ChannelQObject):
156
156
157 # Emitted when the kernel has died.
157 # Emitted when the kernel has died.
158 kernel_died = QtCore.Signal(object)
158 kernel_died = QtCore.Signal(object)
159
159
160 #---------------------------------------------------------------------------
160 #---------------------------------------------------------------------------
161 # 'HBChannel' interface
161 # 'HBChannel' interface
162 #---------------------------------------------------------------------------
162 #---------------------------------------------------------------------------
163
163
164 def call_handlers(self, since_last_heartbeat):
164 def call_handlers(self, since_last_heartbeat):
165 """ Reimplemented to emit signals instead of making callbacks.
165 """ Reimplemented to emit signals instead of making callbacks.
166 """
166 """
167 # Emit the generic signal.
167 # Emit the generic signal.
168 self.kernel_died.emit(since_last_heartbeat)
168 self.kernel_died.emit(since_last_heartbeat)
169
169
170
170
171 class QtKernelRestarterMixin(HasTraits, SuperQObject):
171 class QtKernelRestarterMixin(HasTraits, SuperQObject):
172
172
173 __metaclass__ = MetaQObjectHasTraits
173 __metaclass__ = MetaQObjectHasTraits
174 _timer = None
174 _timer = None
175
175
176
176
177 class QtKernelManagerMixin(HasTraits, SuperQObject):
177 class QtKernelManagerMixin(HasTraits, SuperQObject):
178 """ A KernelClient that provides signals and slots.
178 """ A KernelClient that provides signals and slots.
179 """
179 """
180
180
181 __metaclass__ = MetaQObjectHasTraits
181 __metaclass__ = MetaQObjectHasTraits
182
182
183 kernel_restarted = QtCore.Signal()
183 kernel_restarted = QtCore.Signal()
184
184
185
185
186 class QtKernelClientMixin(HasTraits, SuperQObject):
186 class QtKernelClientMixin(HasTraits, SuperQObject):
187 """ A KernelClient that provides signals and slots.
187 """ A KernelClient that provides signals and slots.
188 """
188 """
189
189
190 __metaclass__ = MetaQObjectHasTraits
190 __metaclass__ = MetaQObjectHasTraits
191
191
192 # Emitted when the kernel client has started listening.
192 # Emitted when the kernel client has started listening.
193 started_channels = QtCore.Signal()
193 started_channels = QtCore.Signal()
194
194
195 # Emitted when the kernel client has stopped listening.
195 # Emitted when the kernel client has stopped listening.
196 stopped_channels = QtCore.Signal()
196 stopped_channels = QtCore.Signal()
197
197
198 # Use Qt-specific channel classes that emit signals.
198 # Use Qt-specific channel classes that emit signals.
199 iopub_channel_class = Type(QtIOPubChannelMixin)
199 iopub_channel_class = Type(QtIOPubChannelMixin)
200 shell_channel_class = Type(QtShellChannelMixin)
200 shell_channel_class = Type(QtShellChannelMixin)
201 stdin_channel_class = Type(QtStdInChannelMixin)
201 stdin_channel_class = Type(QtStdInChannelMixin)
202 hb_channel_class = Type(QtHBChannelMixin)
202 hb_channel_class = Type(QtHBChannelMixin)
203
203
204 #---------------------------------------------------------------------------
204 #---------------------------------------------------------------------------
205 # 'KernelClient' interface
205 # 'KernelClient' interface
206 #---------------------------------------------------------------------------
206 #---------------------------------------------------------------------------
207
207
208 #------ Channel management -------------------------------------------------
208 #------ Channel management -------------------------------------------------
209
209
210 def start_channels(self, *args, **kw):
210 def start_channels(self, *args, **kw):
211 """ Reimplemented to emit signal.
211 """ Reimplemented to emit signal.
212 """
212 """
213 super(QtKernelClientMixin, self).start_channels(*args, **kw)
213 super(QtKernelClientMixin, self).start_channels(*args, **kw)
214 self.started_channels.emit()
214 self.started_channels.emit()
215
215
216 def stop_channels(self):
216 def stop_channels(self):
217 """ Reimplemented to emit signal.
217 """ Reimplemented to emit signal.
218 """
218 """
219 super(QtKernelClientMixin, self).stop_channels()
219 super(QtKernelClientMixin, self).stop_channels()
220 self.stopped_channels.emit()
220 self.stopped_channels.emit()
@@ -1,29 +1,29 b''
1 """Testing support (tools to test IPython itself).
1 """Testing support (tools to test IPython itself).
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2009-2011 The IPython Development Team
5 # Copyright (C) 2009-2011 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Functions
12 # Functions
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 # User-level entry point for testing
15 # User-level entry point for testing
16 def test(all=False):
16 def test(all=False):
17 """Run the entire IPython test suite.
17 """Run the entire IPython test suite.
18
18
19 For fine-grained control, you should use the :file:`iptest` script supplied
19 For fine-grained control, you should use the :file:`iptest` script supplied
20 with the IPython installation."""
20 with the IPython installation."""
21
21
22 # Do the import internally, so that this function doesn't increase total
22 # Do the import internally, so that this function doesn't increase total
23 # import time
23 # import time
24 from iptest import run_iptestall
24 from .iptest import run_iptestall
25 run_iptestall(inc_slow=all)
25 run_iptestall(inc_slow=all)
26
26
27 # So nose doesn't try to run this as a test itself and we end up with an
27 # So nose doesn't try to run this as a test itself and we end up with an
28 # infinite test loop
28 # infinite test loop
29 test.__test__ = False
29 test.__test__ = False
@@ -1,381 +1,381 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Decorators for labeling test objects.
2 """Decorators for labeling test objects.
3
3
4 Decorators that merely return a modified version of the original function
4 Decorators that merely return a modified version of the original function
5 object are straightforward. Decorators that return a new function object need
5 object are straightforward. Decorators that return a new function object need
6 to use nose.tools.make_decorator(original_function)(decorator) in returning the
6 to use nose.tools.make_decorator(original_function)(decorator) in returning the
7 decorator, in order to preserve metadata such as function name, setup and
7 decorator, in order to preserve metadata such as function name, setup and
8 teardown functions and so on - see nose.tools for more information.
8 teardown functions and so on - see nose.tools for more information.
9
9
10 This module provides a set of useful decorators meant to be ready to use in
10 This module provides a set of useful decorators meant to be ready to use in
11 your own tests. See the bottom of the file for the ready-made ones, and if you
11 your own tests. See the bottom of the file for the ready-made ones, and if you
12 find yourself writing a new one that may be of generic use, add it here.
12 find yourself writing a new one that may be of generic use, add it here.
13
13
14 Included decorators:
14 Included decorators:
15
15
16
16
17 Lightweight testing that remains unittest-compatible.
17 Lightweight testing that remains unittest-compatible.
18
18
19 - An @as_unittest decorator can be used to tag any normal parameter-less
19 - An @as_unittest decorator can be used to tag any normal parameter-less
20 function as a unittest TestCase. Then, both nose and normal unittest will
20 function as a unittest TestCase. Then, both nose and normal unittest will
21 recognize it as such. This will make it easier to migrate away from Nose if
21 recognize it as such. This will make it easier to migrate away from Nose if
22 we ever need/want to while maintaining very lightweight tests.
22 we ever need/want to while maintaining very lightweight tests.
23
23
24 NOTE: This file contains IPython-specific decorators. Using the machinery in
24 NOTE: This file contains IPython-specific decorators. Using the machinery in
25 IPython.external.decorators, we import either numpy.testing.decorators if numpy is
25 IPython.external.decorators, we import either numpy.testing.decorators if numpy is
26 available, OR use equivalent code in IPython.external._decorators, which
26 available, OR use equivalent code in IPython.external._decorators, which
27 we've copied verbatim from numpy.
27 we've copied verbatim from numpy.
28
28
29 Authors
29 Authors
30 -------
30 -------
31
31
32 - Fernando Perez <Fernando.Perez@berkeley.edu>
32 - Fernando Perez <Fernando.Perez@berkeley.edu>
33 """
33 """
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Copyright (C) 2009-2011 The IPython Development Team
36 # Copyright (C) 2009-2011 The IPython Development Team
37 #
37 #
38 # Distributed under the terms of the BSD License. The full license is in
38 # Distributed under the terms of the BSD License. The full license is in
39 # the file COPYING, distributed as part of this software.
39 # the file COPYING, distributed as part of this software.
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Imports
43 # Imports
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46 # Stdlib imports
46 # Stdlib imports
47 import sys
47 import sys
48 import os
48 import os
49 import tempfile
49 import tempfile
50 import unittest
50 import unittest
51
51
52 # Third-party imports
52 # Third-party imports
53
53
54 # This is Michele Simionato's decorator module, kept verbatim.
54 # This is Michele Simionato's decorator module, kept verbatim.
55 from IPython.external.decorator import decorator
55 from IPython.external.decorator import decorator
56
56
57 # Expose the unittest-driven decorators
57 # Expose the unittest-driven decorators
58 from ipunittest import ipdoctest, ipdocstring
58 from .ipunittest import ipdoctest, ipdocstring
59
59
60 # Grab the numpy-specific decorators which we keep in a file that we
60 # Grab the numpy-specific decorators which we keep in a file that we
61 # occasionally update from upstream: decorators.py is a copy of
61 # occasionally update from upstream: decorators.py is a copy of
62 # numpy.testing.decorators, we expose all of it here.
62 # numpy.testing.decorators, we expose all of it here.
63 from IPython.external.decorators import *
63 from IPython.external.decorators import *
64
64
65 # For onlyif_cmd_exists decorator
65 # For onlyif_cmd_exists decorator
66 from IPython.utils.process import is_cmd_found
66 from IPython.utils.process import is_cmd_found
67
67
68 #-----------------------------------------------------------------------------
68 #-----------------------------------------------------------------------------
69 # Classes and functions
69 # Classes and functions
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71
71
72 # Simple example of the basic idea
72 # Simple example of the basic idea
73 def as_unittest(func):
73 def as_unittest(func):
74 """Decorator to make a simple function into a normal test via unittest."""
74 """Decorator to make a simple function into a normal test via unittest."""
75 class Tester(unittest.TestCase):
75 class Tester(unittest.TestCase):
76 def test(self):
76 def test(self):
77 func()
77 func()
78
78
79 Tester.__name__ = func.__name__
79 Tester.__name__ = func.__name__
80
80
81 return Tester
81 return Tester
82
82
83 # Utility functions
83 # Utility functions
84
84
85 def apply_wrapper(wrapper,func):
85 def apply_wrapper(wrapper,func):
86 """Apply a wrapper to a function for decoration.
86 """Apply a wrapper to a function for decoration.
87
87
88 This mixes Michele Simionato's decorator tool with nose's make_decorator,
88 This mixes Michele Simionato's decorator tool with nose's make_decorator,
89 to apply a wrapper in a decorator so that all nose attributes, as well as
89 to apply a wrapper in a decorator so that all nose attributes, as well as
90 function signature and other properties, survive the decoration cleanly.
90 function signature and other properties, survive the decoration cleanly.
91 This will ensure that wrapped functions can still be well introspected via
91 This will ensure that wrapped functions can still be well introspected via
92 IPython, for example.
92 IPython, for example.
93 """
93 """
94 import nose.tools
94 import nose.tools
95
95
96 return decorator(wrapper,nose.tools.make_decorator(func)(wrapper))
96 return decorator(wrapper,nose.tools.make_decorator(func)(wrapper))
97
97
98
98
99 def make_label_dec(label,ds=None):
99 def make_label_dec(label,ds=None):
100 """Factory function to create a decorator that applies one or more labels.
100 """Factory function to create a decorator that applies one or more labels.
101
101
102 Parameters
102 Parameters
103 ----------
103 ----------
104 label : string or sequence
104 label : string or sequence
105 One or more labels that will be applied by the decorator to the functions
105 One or more labels that will be applied by the decorator to the functions
106 it decorates. Labels are attributes of the decorated function with their
106 it decorates. Labels are attributes of the decorated function with their
107 value set to True.
107 value set to True.
108
108
109 ds : string
109 ds : string
110 An optional docstring for the resulting decorator. If not given, a
110 An optional docstring for the resulting decorator. If not given, a
111 default docstring is auto-generated.
111 default docstring is auto-generated.
112
112
113 Returns
113 Returns
114 -------
114 -------
115 A decorator.
115 A decorator.
116
116
117 Examples
117 Examples
118 --------
118 --------
119
119
120 A simple labeling decorator:
120 A simple labeling decorator:
121
121
122 >>> slow = make_label_dec('slow')
122 >>> slow = make_label_dec('slow')
123 >>> slow.__doc__
123 >>> slow.__doc__
124 "Labels a test as 'slow'."
124 "Labels a test as 'slow'."
125
125
126 And one that uses multiple labels and a custom docstring:
126 And one that uses multiple labels and a custom docstring:
127
127
128 >>> rare = make_label_dec(['slow','hard'],
128 >>> rare = make_label_dec(['slow','hard'],
129 ... "Mix labels 'slow' and 'hard' for rare tests.")
129 ... "Mix labels 'slow' and 'hard' for rare tests.")
130 >>> rare.__doc__
130 >>> rare.__doc__
131 "Mix labels 'slow' and 'hard' for rare tests."
131 "Mix labels 'slow' and 'hard' for rare tests."
132
132
133 Now, let's test using this one:
133 Now, let's test using this one:
134 >>> @rare
134 >>> @rare
135 ... def f(): pass
135 ... def f(): pass
136 ...
136 ...
137 >>>
137 >>>
138 >>> f.slow
138 >>> f.slow
139 True
139 True
140 >>> f.hard
140 >>> f.hard
141 True
141 True
142 """
142 """
143
143
144 if isinstance(label,basestring):
144 if isinstance(label,basestring):
145 labels = [label]
145 labels = [label]
146 else:
146 else:
147 labels = label
147 labels = label
148
148
149 # Validate that the given label(s) are OK for use in setattr() by doing a
149 # Validate that the given label(s) are OK for use in setattr() by doing a
150 # dry run on a dummy function.
150 # dry run on a dummy function.
151 tmp = lambda : None
151 tmp = lambda : None
152 for label in labels:
152 for label in labels:
153 setattr(tmp,label,True)
153 setattr(tmp,label,True)
154
154
155 # This is the actual decorator we'll return
155 # This is the actual decorator we'll return
156 def decor(f):
156 def decor(f):
157 for label in labels:
157 for label in labels:
158 setattr(f,label,True)
158 setattr(f,label,True)
159 return f
159 return f
160
160
161 # Apply the user's docstring, or autogenerate a basic one
161 # Apply the user's docstring, or autogenerate a basic one
162 if ds is None:
162 if ds is None:
163 ds = "Labels a test as %r." % label
163 ds = "Labels a test as %r." % label
164 decor.__doc__ = ds
164 decor.__doc__ = ds
165
165
166 return decor
166 return decor
167
167
168
168
169 # Inspired by numpy's skipif, but uses the full apply_wrapper utility to
169 # Inspired by numpy's skipif, but uses the full apply_wrapper utility to
170 # preserve function metadata better and allows the skip condition to be a
170 # preserve function metadata better and allows the skip condition to be a
171 # callable.
171 # callable.
172 def skipif(skip_condition, msg=None):
172 def skipif(skip_condition, msg=None):
173 ''' Make function raise SkipTest exception if skip_condition is true
173 ''' Make function raise SkipTest exception if skip_condition is true
174
174
175 Parameters
175 Parameters
176 ----------
176 ----------
177 skip_condition : bool or callable.
177 skip_condition : bool or callable.
178 Flag to determine whether to skip test. If the condition is a
178 Flag to determine whether to skip test. If the condition is a
179 callable, it is used at runtime to dynamically make the decision. This
179 callable, it is used at runtime to dynamically make the decision. This
180 is useful for tests that may require costly imports, to delay the cost
180 is useful for tests that may require costly imports, to delay the cost
181 until the test suite is actually executed.
181 until the test suite is actually executed.
182 msg : string
182 msg : string
183 Message to give on raising a SkipTest exception
183 Message to give on raising a SkipTest exception
184
184
185 Returns
185 Returns
186 -------
186 -------
187 decorator : function
187 decorator : function
188 Decorator, which, when applied to a function, causes SkipTest
188 Decorator, which, when applied to a function, causes SkipTest
189 to be raised when the skip_condition was True, and the function
189 to be raised when the skip_condition was True, and the function
190 to be called normally otherwise.
190 to be called normally otherwise.
191
191
192 Notes
192 Notes
193 -----
193 -----
194 You will see from the code that we had to further decorate the
194 You will see from the code that we had to further decorate the
195 decorator with the nose.tools.make_decorator function in order to
195 decorator with the nose.tools.make_decorator function in order to
196 transmit function name, and various other metadata.
196 transmit function name, and various other metadata.
197 '''
197 '''
198
198
199 def skip_decorator(f):
199 def skip_decorator(f):
200 # Local import to avoid a hard nose dependency and only incur the
200 # Local import to avoid a hard nose dependency and only incur the
201 # import time overhead at actual test-time.
201 # import time overhead at actual test-time.
202 import nose
202 import nose
203
203
204 # Allow for both boolean or callable skip conditions.
204 # Allow for both boolean or callable skip conditions.
205 if callable(skip_condition):
205 if callable(skip_condition):
206 skip_val = skip_condition
206 skip_val = skip_condition
207 else:
207 else:
208 skip_val = lambda : skip_condition
208 skip_val = lambda : skip_condition
209
209
210 def get_msg(func,msg=None):
210 def get_msg(func,msg=None):
211 """Skip message with information about function being skipped."""
211 """Skip message with information about function being skipped."""
212 if msg is None: out = 'Test skipped due to test condition.'
212 if msg is None: out = 'Test skipped due to test condition.'
213 else: out = msg
213 else: out = msg
214 return "Skipping test: %s. %s" % (func.__name__,out)
214 return "Skipping test: %s. %s" % (func.__name__,out)
215
215
216 # We need to define *two* skippers because Python doesn't allow both
216 # We need to define *two* skippers because Python doesn't allow both
217 # return with value and yield inside the same function.
217 # return with value and yield inside the same function.
218 def skipper_func(*args, **kwargs):
218 def skipper_func(*args, **kwargs):
219 """Skipper for normal test functions."""
219 """Skipper for normal test functions."""
220 if skip_val():
220 if skip_val():
221 raise nose.SkipTest(get_msg(f,msg))
221 raise nose.SkipTest(get_msg(f,msg))
222 else:
222 else:
223 return f(*args, **kwargs)
223 return f(*args, **kwargs)
224
224
225 def skipper_gen(*args, **kwargs):
225 def skipper_gen(*args, **kwargs):
226 """Skipper for test generators."""
226 """Skipper for test generators."""
227 if skip_val():
227 if skip_val():
228 raise nose.SkipTest(get_msg(f,msg))
228 raise nose.SkipTest(get_msg(f,msg))
229 else:
229 else:
230 for x in f(*args, **kwargs):
230 for x in f(*args, **kwargs):
231 yield x
231 yield x
232
232
233 # Choose the right skipper to use when building the actual generator.
233 # Choose the right skipper to use when building the actual generator.
234 if nose.util.isgenerator(f):
234 if nose.util.isgenerator(f):
235 skipper = skipper_gen
235 skipper = skipper_gen
236 else:
236 else:
237 skipper = skipper_func
237 skipper = skipper_func
238
238
239 return nose.tools.make_decorator(f)(skipper)
239 return nose.tools.make_decorator(f)(skipper)
240
240
241 return skip_decorator
241 return skip_decorator
242
242
243 # A version with the condition set to true, common case just to attach a message
243 # A version with the condition set to true, common case just to attach a message
244 # to a skip decorator
244 # to a skip decorator
245 def skip(msg=None):
245 def skip(msg=None):
246 """Decorator factory - mark a test function for skipping from test suite.
246 """Decorator factory - mark a test function for skipping from test suite.
247
247
248 Parameters
248 Parameters
249 ----------
249 ----------
250 msg : string
250 msg : string
251 Optional message to be added.
251 Optional message to be added.
252
252
253 Returns
253 Returns
254 -------
254 -------
255 decorator : function
255 decorator : function
256 Decorator, which, when applied to a function, causes SkipTest
256 Decorator, which, when applied to a function, causes SkipTest
257 to be raised, with the optional message added.
257 to be raised, with the optional message added.
258 """
258 """
259
259
260 return skipif(True,msg)
260 return skipif(True,msg)
261
261
262
262
263 def onlyif(condition, msg):
263 def onlyif(condition, msg):
264 """The reverse from skipif, see skipif for details."""
264 """The reverse from skipif, see skipif for details."""
265
265
266 if callable(condition):
266 if callable(condition):
267 skip_condition = lambda : not condition()
267 skip_condition = lambda : not condition()
268 else:
268 else:
269 skip_condition = lambda : not condition
269 skip_condition = lambda : not condition
270
270
271 return skipif(skip_condition, msg)
271 return skipif(skip_condition, msg)
272
272
273 #-----------------------------------------------------------------------------
273 #-----------------------------------------------------------------------------
274 # Utility functions for decorators
274 # Utility functions for decorators
275 def module_not_available(module):
275 def module_not_available(module):
276 """Can module be imported? Returns true if module does NOT import.
276 """Can module be imported? Returns true if module does NOT import.
277
277
278 This is used to make a decorator to skip tests that require module to be
278 This is used to make a decorator to skip tests that require module to be
279 available, but delay the 'import numpy' to test execution time.
279 available, but delay the 'import numpy' to test execution time.
280 """
280 """
281 try:
281 try:
282 mod = __import__(module)
282 mod = __import__(module)
283 mod_not_avail = False
283 mod_not_avail = False
284 except ImportError:
284 except ImportError:
285 mod_not_avail = True
285 mod_not_avail = True
286
286
287 return mod_not_avail
287 return mod_not_avail
288
288
289
289
290 def decorated_dummy(dec, name):
290 def decorated_dummy(dec, name):
291 """Return a dummy function decorated with dec, with the given name.
291 """Return a dummy function decorated with dec, with the given name.
292
292
293 Examples
293 Examples
294 --------
294 --------
295 import IPython.testing.decorators as dec
295 import IPython.testing.decorators as dec
296 setup = dec.decorated_dummy(dec.skip_if_no_x11, __name__)
296 setup = dec.decorated_dummy(dec.skip_if_no_x11, __name__)
297 """
297 """
298 dummy = lambda: None
298 dummy = lambda: None
299 dummy.__name__ = name
299 dummy.__name__ = name
300 return dec(dummy)
300 return dec(dummy)
301
301
302 #-----------------------------------------------------------------------------
302 #-----------------------------------------------------------------------------
303 # Decorators for public use
303 # Decorators for public use
304
304
305 # Decorators to skip certain tests on specific platforms.
305 # Decorators to skip certain tests on specific platforms.
306 skip_win32 = skipif(sys.platform == 'win32',
306 skip_win32 = skipif(sys.platform == 'win32',
307 "This test does not run under Windows")
307 "This test does not run under Windows")
308 skip_linux = skipif(sys.platform.startswith('linux'),
308 skip_linux = skipif(sys.platform.startswith('linux'),
309 "This test does not run under Linux")
309 "This test does not run under Linux")
310 skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X")
310 skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X")
311
311
312
312
313 # Decorators to skip tests if not on specific platforms.
313 # Decorators to skip tests if not on specific platforms.
314 skip_if_not_win32 = skipif(sys.platform != 'win32',
314 skip_if_not_win32 = skipif(sys.platform != 'win32',
315 "This test only runs under Windows")
315 "This test only runs under Windows")
316 skip_if_not_linux = skipif(not sys.platform.startswith('linux'),
316 skip_if_not_linux = skipif(not sys.platform.startswith('linux'),
317 "This test only runs under Linux")
317 "This test only runs under Linux")
318 skip_if_not_osx = skipif(sys.platform != 'darwin',
318 skip_if_not_osx = skipif(sys.platform != 'darwin',
319 "This test only runs under OSX")
319 "This test only runs under OSX")
320
320
321
321
322 _x11_skip_cond = (sys.platform not in ('darwin', 'win32') and
322 _x11_skip_cond = (sys.platform not in ('darwin', 'win32') and
323 os.environ.get('DISPLAY', '') == '')
323 os.environ.get('DISPLAY', '') == '')
324 _x11_skip_msg = "Skipped under *nix when X11/XOrg not available"
324 _x11_skip_msg = "Skipped under *nix when X11/XOrg not available"
325
325
326 skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg)
326 skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg)
327
327
328 # not a decorator itself, returns a dummy function to be used as setup
328 # not a decorator itself, returns a dummy function to be used as setup
329 def skip_file_no_x11(name):
329 def skip_file_no_x11(name):
330 return decorated_dummy(skip_if_no_x11, name) if _x11_skip_cond else None
330 return decorated_dummy(skip_if_no_x11, name) if _x11_skip_cond else None
331
331
332 # Other skip decorators
332 # Other skip decorators
333
333
334 # generic skip without module
334 # generic skip without module
335 skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod)
335 skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod)
336
336
337 skipif_not_numpy = skip_without('numpy')
337 skipif_not_numpy = skip_without('numpy')
338
338
339 skipif_not_matplotlib = skip_without('matplotlib')
339 skipif_not_matplotlib = skip_without('matplotlib')
340
340
341 skipif_not_sympy = skip_without('sympy')
341 skipif_not_sympy = skip_without('sympy')
342
342
343 skip_known_failure = knownfailureif(True,'This test is known to fail')
343 skip_known_failure = knownfailureif(True,'This test is known to fail')
344
344
345 known_failure_py3 = knownfailureif(sys.version_info[0] >= 3,
345 known_failure_py3 = knownfailureif(sys.version_info[0] >= 3,
346 'This test is known to fail on Python 3.')
346 'This test is known to fail on Python 3.')
347
347
348 # A null 'decorator', useful to make more readable code that needs to pick
348 # A null 'decorator', useful to make more readable code that needs to pick
349 # between different decorators based on OS or other conditions
349 # between different decorators based on OS or other conditions
350 null_deco = lambda f: f
350 null_deco = lambda f: f
351
351
352 # Some tests only run where we can use unicode paths. Note that we can't just
352 # Some tests only run where we can use unicode paths. Note that we can't just
353 # check os.path.supports_unicode_filenames, which is always False on Linux.
353 # check os.path.supports_unicode_filenames, which is always False on Linux.
354 try:
354 try:
355 f = tempfile.NamedTemporaryFile(prefix=u"tmp€")
355 f = tempfile.NamedTemporaryFile(prefix=u"tmp€")
356 except UnicodeEncodeError:
356 except UnicodeEncodeError:
357 unicode_paths = False
357 unicode_paths = False
358 else:
358 else:
359 unicode_paths = True
359 unicode_paths = True
360 f.close()
360 f.close()
361
361
362 onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable "
362 onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable "
363 "where we can use unicode in filenames."))
363 "where we can use unicode in filenames."))
364
364
365
365
366 def onlyif_cmds_exist(*commands):
366 def onlyif_cmds_exist(*commands):
367 """
367 """
368 Decorator to skip test when at least one of `commands` is not found.
368 Decorator to skip test when at least one of `commands` is not found.
369 """
369 """
370 for cmd in commands:
370 for cmd in commands:
371 try:
371 try:
372 if not is_cmd_found(cmd):
372 if not is_cmd_found(cmd):
373 return skip("This test runs only if command '{0}' "
373 return skip("This test runs only if command '{0}' "
374 "is installed".format(cmd))
374 "is installed".format(cmd))
375 except ImportError as e:
375 except ImportError as e:
376 # is_cmd_found uses pywin32 on windows, which might not be available
376 # is_cmd_found uses pywin32 on windows, which might not be available
377 if sys.platform == 'win32' and 'pywin32' in str(e):
377 if sys.platform == 'win32' and 'pywin32' in str(e):
378 return skip("This test runs only if pywin32 and command '{0}' "
378 return skip("This test runs only if pywin32 and command '{0}' "
379 "is installed".format(cmd))
379 "is installed".format(cmd))
380 raise e
380 raise e
381 return null_deco
381 return null_deco
@@ -1,18 +1,18 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """Nose-based test runner.
2 """Nose-based test runner.
3 """
3 """
4
4
5 from nose.core import main
5 from nose.core import main
6 from nose.plugins.builtin import plugins
6 from nose.plugins.builtin import plugins
7 from nose.plugins.doctests import Doctest
7 from nose.plugins.doctests import Doctest
8
8
9 import ipdoctest
9 from . import ipdoctest
10 from ipdoctest import IPDocTestRunner
10 from .ipdoctest import IPDocTestRunner
11
11
12 if __name__ == '__main__':
12 if __name__ == '__main__':
13 print 'WARNING: this code is incomplete!'
13 print 'WARNING: this code is incomplete!'
14 print
14 print
15
15
16 pp = [x() for x in plugins] # activate all builtin plugins first
16 pp = [x() for x in plugins] # activate all builtin plugins first
17 main(testRunner=IPDocTestRunner(),
17 main(testRunner=IPDocTestRunner(),
18 plugins=pp+[ipdoctest.IPythonDoctest(),Doctest()])
18 plugins=pp+[ipdoctest.IPythonDoctest(),Doctest()])
@@ -1,351 +1,351 b''
1 # encoding: utf-8
1 # encoding: utf-8
2
2
3 """Pickle related utilities. Perhaps this should be called 'can'."""
3 """Pickle related utilities. Perhaps this should be called 'can'."""
4
4
5 __docformat__ = "restructuredtext en"
5 __docformat__ = "restructuredtext en"
6
6
7 #-------------------------------------------------------------------------------
7 #-------------------------------------------------------------------------------
8 # Copyright (C) 2008-2011 The IPython Development Team
8 # Copyright (C) 2008-2011 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
11 # the file COPYING, distributed as part of this software.
12 #-------------------------------------------------------------------------------
12 #-------------------------------------------------------------------------------
13
13
14 #-------------------------------------------------------------------------------
14 #-------------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-------------------------------------------------------------------------------
16 #-------------------------------------------------------------------------------
17
17
18 import copy
18 import copy
19 import logging
19 import logging
20 import sys
20 import sys
21 from types import FunctionType
21 from types import FunctionType
22
22
23 try:
23 try:
24 import cPickle as pickle
24 import cPickle as pickle
25 except ImportError:
25 except ImportError:
26 import pickle
26 import pickle
27
27
28 import codeutil # This registers a hook when it's imported
28 from . import codeutil # This registers a hook when it's imported
29 import py3compat
29 from . import py3compat
30 from importstring import import_item
30 from .importstring import import_item
31
31
32 from IPython.config import Application
32 from IPython.config import Application
33
33
34 if py3compat.PY3:
34 if py3compat.PY3:
35 buffer = memoryview
35 buffer = memoryview
36 class_type = type
36 class_type = type
37 else:
37 else:
38 from types import ClassType
38 from types import ClassType
39 class_type = (type, ClassType)
39 class_type = (type, ClassType)
40
40
41 #-------------------------------------------------------------------------------
41 #-------------------------------------------------------------------------------
42 # Classes
42 # Classes
43 #-------------------------------------------------------------------------------
43 #-------------------------------------------------------------------------------
44
44
45
45
46 class CannedObject(object):
46 class CannedObject(object):
47 def __init__(self, obj, keys=[], hook=None):
47 def __init__(self, obj, keys=[], hook=None):
48 """can an object for safe pickling
48 """can an object for safe pickling
49
49
50 Parameters
50 Parameters
51 ==========
51 ==========
52
52
53 obj:
53 obj:
54 The object to be canned
54 The object to be canned
55 keys: list (optional)
55 keys: list (optional)
56 list of attribute names that will be explicitly canned / uncanned
56 list of attribute names that will be explicitly canned / uncanned
57 hook: callable (optional)
57 hook: callable (optional)
58 An optional extra callable,
58 An optional extra callable,
59 which can do additional processing of the uncanned object.
59 which can do additional processing of the uncanned object.
60
60
61 large data may be offloaded into the buffers list,
61 large data may be offloaded into the buffers list,
62 used for zero-copy transfers.
62 used for zero-copy transfers.
63 """
63 """
64 self.keys = keys
64 self.keys = keys
65 self.obj = copy.copy(obj)
65 self.obj = copy.copy(obj)
66 self.hook = can(hook)
66 self.hook = can(hook)
67 for key in keys:
67 for key in keys:
68 setattr(self.obj, key, can(getattr(obj, key)))
68 setattr(self.obj, key, can(getattr(obj, key)))
69
69
70 self.buffers = []
70 self.buffers = []
71
71
72 def get_object(self, g=None):
72 def get_object(self, g=None):
73 if g is None:
73 if g is None:
74 g = {}
74 g = {}
75 obj = self.obj
75 obj = self.obj
76 for key in self.keys:
76 for key in self.keys:
77 setattr(obj, key, uncan(getattr(obj, key), g))
77 setattr(obj, key, uncan(getattr(obj, key), g))
78
78
79 if self.hook:
79 if self.hook:
80 self.hook = uncan(self.hook, g)
80 self.hook = uncan(self.hook, g)
81 self.hook(obj, g)
81 self.hook(obj, g)
82 return self.obj
82 return self.obj
83
83
84
84
85 class Reference(CannedObject):
85 class Reference(CannedObject):
86 """object for wrapping a remote reference by name."""
86 """object for wrapping a remote reference by name."""
87 def __init__(self, name):
87 def __init__(self, name):
88 if not isinstance(name, basestring):
88 if not isinstance(name, basestring):
89 raise TypeError("illegal name: %r"%name)
89 raise TypeError("illegal name: %r"%name)
90 self.name = name
90 self.name = name
91 self.buffers = []
91 self.buffers = []
92
92
93 def __repr__(self):
93 def __repr__(self):
94 return "<Reference: %r>"%self.name
94 return "<Reference: %r>"%self.name
95
95
96 def get_object(self, g=None):
96 def get_object(self, g=None):
97 if g is None:
97 if g is None:
98 g = {}
98 g = {}
99
99
100 return eval(self.name, g)
100 return eval(self.name, g)
101
101
102
102
103 class CannedFunction(CannedObject):
103 class CannedFunction(CannedObject):
104
104
105 def __init__(self, f):
105 def __init__(self, f):
106 self._check_type(f)
106 self._check_type(f)
107 self.code = f.func_code
107 self.code = f.func_code
108 if f.func_defaults:
108 if f.func_defaults:
109 self.defaults = [ can(fd) for fd in f.func_defaults ]
109 self.defaults = [ can(fd) for fd in f.func_defaults ]
110 else:
110 else:
111 self.defaults = None
111 self.defaults = None
112 self.module = f.__module__ or '__main__'
112 self.module = f.__module__ or '__main__'
113 self.__name__ = f.__name__
113 self.__name__ = f.__name__
114 self.buffers = []
114 self.buffers = []
115
115
116 def _check_type(self, obj):
116 def _check_type(self, obj):
117 assert isinstance(obj, FunctionType), "Not a function type"
117 assert isinstance(obj, FunctionType), "Not a function type"
118
118
119 def get_object(self, g=None):
119 def get_object(self, g=None):
120 # try to load function back into its module:
120 # try to load function back into its module:
121 if not self.module.startswith('__'):
121 if not self.module.startswith('__'):
122 __import__(self.module)
122 __import__(self.module)
123 g = sys.modules[self.module].__dict__
123 g = sys.modules[self.module].__dict__
124
124
125 if g is None:
125 if g is None:
126 g = {}
126 g = {}
127 if self.defaults:
127 if self.defaults:
128 defaults = tuple(uncan(cfd, g) for cfd in self.defaults)
128 defaults = tuple(uncan(cfd, g) for cfd in self.defaults)
129 else:
129 else:
130 defaults = None
130 defaults = None
131 newFunc = FunctionType(self.code, g, self.__name__, defaults)
131 newFunc = FunctionType(self.code, g, self.__name__, defaults)
132 return newFunc
132 return newFunc
133
133
134 class CannedClass(CannedObject):
134 class CannedClass(CannedObject):
135
135
136 def __init__(self, cls):
136 def __init__(self, cls):
137 self._check_type(cls)
137 self._check_type(cls)
138 self.name = cls.__name__
138 self.name = cls.__name__
139 self.old_style = not isinstance(cls, type)
139 self.old_style = not isinstance(cls, type)
140 self._canned_dict = {}
140 self._canned_dict = {}
141 for k,v in cls.__dict__.items():
141 for k,v in cls.__dict__.items():
142 if k not in ('__weakref__', '__dict__'):
142 if k not in ('__weakref__', '__dict__'):
143 self._canned_dict[k] = can(v)
143 self._canned_dict[k] = can(v)
144 if self.old_style:
144 if self.old_style:
145 mro = []
145 mro = []
146 else:
146 else:
147 mro = cls.mro()
147 mro = cls.mro()
148
148
149 self.parents = [ can(c) for c in mro[1:] ]
149 self.parents = [ can(c) for c in mro[1:] ]
150 self.buffers = []
150 self.buffers = []
151
151
152 def _check_type(self, obj):
152 def _check_type(self, obj):
153 assert isinstance(obj, class_type), "Not a class type"
153 assert isinstance(obj, class_type), "Not a class type"
154
154
155 def get_object(self, g=None):
155 def get_object(self, g=None):
156 parents = tuple(uncan(p, g) for p in self.parents)
156 parents = tuple(uncan(p, g) for p in self.parents)
157 return type(self.name, parents, uncan_dict(self._canned_dict, g=g))
157 return type(self.name, parents, uncan_dict(self._canned_dict, g=g))
158
158
159 class CannedArray(CannedObject):
159 class CannedArray(CannedObject):
160 def __init__(self, obj):
160 def __init__(self, obj):
161 from numpy import ascontiguousarray
161 from numpy import ascontiguousarray
162 self.shape = obj.shape
162 self.shape = obj.shape
163 self.dtype = obj.dtype.descr if obj.dtype.fields else obj.dtype.str
163 self.dtype = obj.dtype.descr if obj.dtype.fields else obj.dtype.str
164 if sum(obj.shape) == 0:
164 if sum(obj.shape) == 0:
165 # just pickle it
165 # just pickle it
166 self.buffers = [pickle.dumps(obj, -1)]
166 self.buffers = [pickle.dumps(obj, -1)]
167 else:
167 else:
168 # ensure contiguous
168 # ensure contiguous
169 obj = ascontiguousarray(obj, dtype=None)
169 obj = ascontiguousarray(obj, dtype=None)
170 self.buffers = [buffer(obj)]
170 self.buffers = [buffer(obj)]
171
171
172 def get_object(self, g=None):
172 def get_object(self, g=None):
173 from numpy import frombuffer
173 from numpy import frombuffer
174 data = self.buffers[0]
174 data = self.buffers[0]
175 if sum(self.shape) == 0:
175 if sum(self.shape) == 0:
176 # no shape, we just pickled it
176 # no shape, we just pickled it
177 return pickle.loads(data)
177 return pickle.loads(data)
178 else:
178 else:
179 return frombuffer(data, dtype=self.dtype).reshape(self.shape)
179 return frombuffer(data, dtype=self.dtype).reshape(self.shape)
180
180
181
181
182 class CannedBytes(CannedObject):
182 class CannedBytes(CannedObject):
183 wrap = bytes
183 wrap = bytes
184 def __init__(self, obj):
184 def __init__(self, obj):
185 self.buffers = [obj]
185 self.buffers = [obj]
186
186
187 def get_object(self, g=None):
187 def get_object(self, g=None):
188 data = self.buffers[0]
188 data = self.buffers[0]
189 return self.wrap(data)
189 return self.wrap(data)
190
190
191 def CannedBuffer(CannedBytes):
191 def CannedBuffer(CannedBytes):
192 wrap = buffer
192 wrap = buffer
193
193
194 #-------------------------------------------------------------------------------
194 #-------------------------------------------------------------------------------
195 # Functions
195 # Functions
196 #-------------------------------------------------------------------------------
196 #-------------------------------------------------------------------------------
197
197
198 def _logger():
198 def _logger():
199 """get the logger for the current Application
199 """get the logger for the current Application
200
200
201 the root logger will be used if no Application is running
201 the root logger will be used if no Application is running
202 """
202 """
203 if Application.initialized():
203 if Application.initialized():
204 logger = Application.instance().log
204 logger = Application.instance().log
205 else:
205 else:
206 logger = logging.getLogger()
206 logger = logging.getLogger()
207 if not logger.handlers:
207 if not logger.handlers:
208 logging.basicConfig()
208 logging.basicConfig()
209
209
210 return logger
210 return logger
211
211
212 def _import_mapping(mapping, original=None):
212 def _import_mapping(mapping, original=None):
213 """import any string-keys in a type mapping
213 """import any string-keys in a type mapping
214
214
215 """
215 """
216 log = _logger()
216 log = _logger()
217 log.debug("Importing canning map")
217 log.debug("Importing canning map")
218 for key,value in mapping.items():
218 for key,value in mapping.items():
219 if isinstance(key, basestring):
219 if isinstance(key, basestring):
220 try:
220 try:
221 cls = import_item(key)
221 cls = import_item(key)
222 except Exception:
222 except Exception:
223 if original and key not in original:
223 if original and key not in original:
224 # only message on user-added classes
224 # only message on user-added classes
225 log.error("canning class not importable: %r", key, exc_info=True)
225 log.error("canning class not importable: %r", key, exc_info=True)
226 mapping.pop(key)
226 mapping.pop(key)
227 else:
227 else:
228 mapping[cls] = mapping.pop(key)
228 mapping[cls] = mapping.pop(key)
229
229
230 def istype(obj, check):
230 def istype(obj, check):
231 """like isinstance(obj, check), but strict
231 """like isinstance(obj, check), but strict
232
232
233 This won't catch subclasses.
233 This won't catch subclasses.
234 """
234 """
235 if isinstance(check, tuple):
235 if isinstance(check, tuple):
236 for cls in check:
236 for cls in check:
237 if type(obj) is cls:
237 if type(obj) is cls:
238 return True
238 return True
239 return False
239 return False
240 else:
240 else:
241 return type(obj) is check
241 return type(obj) is check
242
242
243 def can(obj):
243 def can(obj):
244 """prepare an object for pickling"""
244 """prepare an object for pickling"""
245
245
246 import_needed = False
246 import_needed = False
247
247
248 for cls,canner in can_map.iteritems():
248 for cls,canner in can_map.iteritems():
249 if isinstance(cls, basestring):
249 if isinstance(cls, basestring):
250 import_needed = True
250 import_needed = True
251 break
251 break
252 elif istype(obj, cls):
252 elif istype(obj, cls):
253 return canner(obj)
253 return canner(obj)
254
254
255 if import_needed:
255 if import_needed:
256 # perform can_map imports, then try again
256 # perform can_map imports, then try again
257 # this will usually only happen once
257 # this will usually only happen once
258 _import_mapping(can_map, _original_can_map)
258 _import_mapping(can_map, _original_can_map)
259 return can(obj)
259 return can(obj)
260
260
261 return obj
261 return obj
262
262
263 def can_class(obj):
263 def can_class(obj):
264 if isinstance(obj, class_type) and obj.__module__ == '__main__':
264 if isinstance(obj, class_type) and obj.__module__ == '__main__':
265 return CannedClass(obj)
265 return CannedClass(obj)
266 else:
266 else:
267 return obj
267 return obj
268
268
269 def can_dict(obj):
269 def can_dict(obj):
270 """can the *values* of a dict"""
270 """can the *values* of a dict"""
271 if istype(obj, dict):
271 if istype(obj, dict):
272 newobj = {}
272 newobj = {}
273 for k, v in obj.iteritems():
273 for k, v in obj.iteritems():
274 newobj[k] = can(v)
274 newobj[k] = can(v)
275 return newobj
275 return newobj
276 else:
276 else:
277 return obj
277 return obj
278
278
279 sequence_types = (list, tuple, set)
279 sequence_types = (list, tuple, set)
280
280
281 def can_sequence(obj):
281 def can_sequence(obj):
282 """can the elements of a sequence"""
282 """can the elements of a sequence"""
283 if istype(obj, sequence_types):
283 if istype(obj, sequence_types):
284 t = type(obj)
284 t = type(obj)
285 return t([can(i) for i in obj])
285 return t([can(i) for i in obj])
286 else:
286 else:
287 return obj
287 return obj
288
288
289 def uncan(obj, g=None):
289 def uncan(obj, g=None):
290 """invert canning"""
290 """invert canning"""
291
291
292 import_needed = False
292 import_needed = False
293 for cls,uncanner in uncan_map.iteritems():
293 for cls,uncanner in uncan_map.iteritems():
294 if isinstance(cls, basestring):
294 if isinstance(cls, basestring):
295 import_needed = True
295 import_needed = True
296 break
296 break
297 elif isinstance(obj, cls):
297 elif isinstance(obj, cls):
298 return uncanner(obj, g)
298 return uncanner(obj, g)
299
299
300 if import_needed:
300 if import_needed:
301 # perform uncan_map imports, then try again
301 # perform uncan_map imports, then try again
302 # this will usually only happen once
302 # this will usually only happen once
303 _import_mapping(uncan_map, _original_uncan_map)
303 _import_mapping(uncan_map, _original_uncan_map)
304 return uncan(obj, g)
304 return uncan(obj, g)
305
305
306 return obj
306 return obj
307
307
308 def uncan_dict(obj, g=None):
308 def uncan_dict(obj, g=None):
309 if istype(obj, dict):
309 if istype(obj, dict):
310 newobj = {}
310 newobj = {}
311 for k, v in obj.iteritems():
311 for k, v in obj.iteritems():
312 newobj[k] = uncan(v,g)
312 newobj[k] = uncan(v,g)
313 return newobj
313 return newobj
314 else:
314 else:
315 return obj
315 return obj
316
316
317 def uncan_sequence(obj, g=None):
317 def uncan_sequence(obj, g=None):
318 if istype(obj, sequence_types):
318 if istype(obj, sequence_types):
319 t = type(obj)
319 t = type(obj)
320 return t([uncan(i,g) for i in obj])
320 return t([uncan(i,g) for i in obj])
321 else:
321 else:
322 return obj
322 return obj
323
323
324 def _uncan_dependent_hook(dep, g=None):
324 def _uncan_dependent_hook(dep, g=None):
325 dep.check_dependency()
325 dep.check_dependency()
326
326
327 def can_dependent(obj):
327 def can_dependent(obj):
328 return CannedObject(obj, keys=('f', 'df'), hook=_uncan_dependent_hook)
328 return CannedObject(obj, keys=('f', 'df'), hook=_uncan_dependent_hook)
329
329
330 #-------------------------------------------------------------------------------
330 #-------------------------------------------------------------------------------
331 # API dictionaries
331 # API dictionaries
332 #-------------------------------------------------------------------------------
332 #-------------------------------------------------------------------------------
333
333
334 # These dicts can be extended for custom serialization of new objects
334 # These dicts can be extended for custom serialization of new objects
335
335
336 can_map = {
336 can_map = {
337 'IPython.parallel.dependent' : can_dependent,
337 'IPython.parallel.dependent' : can_dependent,
338 'numpy.ndarray' : CannedArray,
338 'numpy.ndarray' : CannedArray,
339 FunctionType : CannedFunction,
339 FunctionType : CannedFunction,
340 bytes : CannedBytes,
340 bytes : CannedBytes,
341 buffer : CannedBuffer,
341 buffer : CannedBuffer,
342 class_type : can_class,
342 class_type : can_class,
343 }
343 }
344
344
345 uncan_map = {
345 uncan_map = {
346 CannedObject : lambda obj, g: obj.get_object(g),
346 CannedObject : lambda obj, g: obj.get_object(g),
347 }
347 }
348
348
349 # for use in _import_mapping:
349 # for use in _import_mapping:
350 _original_can_map = can_map.copy()
350 _original_can_map = can_map.copy()
351 _original_uncan_map = uncan_map.copy()
351 _original_uncan_map = uncan_map.copy()
@@ -1,9 +1,9 b''
1 """Load our patched versions of tokenize.
1 """Load our patched versions of tokenize.
2 """
2 """
3
3
4 import sys
4 import sys
5
5
6 if sys.version_info[0] >= 3:
6 if sys.version_info[0] >= 3:
7 from _tokenize_py3 import *
7 from ._tokenize_py3 import *
8 else:
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