##// END OF EJS Templates
Merge pull request #7129 from ccordoba12/pyqt5...
Thomas Kluyver -
r19568:bd69cc1f merge
parent child Browse files
Show More
@@ -1,203 +1,203 b''
1 """Base classes to manage a Client's interaction with a running kernel"""
1 """Base classes to manage a Client's interaction with a running kernel"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 import atexit
8 import atexit
9 import errno
9 import errno
10 from threading import Thread
10 from threading import Thread
11 import time
11 import time
12
12
13 import zmq
13 import zmq
14 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
14 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
15 # during garbage collection of threads at exit:
15 # during garbage collection of threads at exit:
16 from zmq import ZMQError
16 from zmq import ZMQError
17
17
18 from IPython.core.release import kernel_protocol_version_info
18 from IPython.core.release import kernel_protocol_version_info
19
19
20 from .channelsabc import HBChannelABC
20 from .channelsabc import HBChannelABC
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Constants and exceptions
23 # Constants and exceptions
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25
25
26 major_protocol_version = kernel_protocol_version_info[0]
26 major_protocol_version = kernel_protocol_version_info[0]
27
27
28 class InvalidPortNumber(Exception):
28 class InvalidPortNumber(Exception):
29 pass
29 pass
30
30
31 class HBChannel(Thread):
31 class HBChannel(Thread):
32 """The heartbeat channel which monitors the kernel heartbeat.
32 """The heartbeat channel which monitors the kernel heartbeat.
33
33
34 Note that the heartbeat channel is paused by default. As long as you start
34 Note that the heartbeat channel is paused by default. As long as you start
35 this channel, the kernel manager will ensure that it is paused and un-paused
35 this channel, the kernel manager will ensure that it is paused and un-paused
36 as appropriate.
36 as appropriate.
37 """
37 """
38 context = None
38 context = None
39 session = None
39 session = None
40 socket = None
40 socket = None
41 address = None
41 address = None
42 _exiting = False
42 _exiting = False
43
43
44 time_to_dead = 1.
44 time_to_dead = 1.
45 poller = None
45 poller = None
46 _running = None
46 _running = None
47 _pause = None
47 _pause = None
48 _beating = None
48 _beating = None
49
49
50 def __init__(self, context, session, address):
50 def __init__(self, context=None, session=None, address=None):
51 """Create the heartbeat monitor thread.
51 """Create the heartbeat monitor thread.
52
52
53 Parameters
53 Parameters
54 ----------
54 ----------
55 context : :class:`zmq.Context`
55 context : :class:`zmq.Context`
56 The ZMQ context to use.
56 The ZMQ context to use.
57 session : :class:`session.Session`
57 session : :class:`session.Session`
58 The session to use.
58 The session to use.
59 address : zmq url
59 address : zmq url
60 Standard (ip, port) tuple that the kernel is listening on.
60 Standard (ip, port) tuple that the kernel is listening on.
61 """
61 """
62 super(HBChannel, self).__init__()
62 super(HBChannel, self).__init__()
63 self.daemon = True
63 self.daemon = True
64
64
65 self.context = context
65 self.context = context
66 self.session = session
66 self.session = session
67 if isinstance(address, tuple):
67 if isinstance(address, tuple):
68 if address[1] == 0:
68 if address[1] == 0:
69 message = 'The port number for a channel cannot be 0.'
69 message = 'The port number for a channel cannot be 0.'
70 raise InvalidPortNumber(message)
70 raise InvalidPortNumber(message)
71 address = "tcp://%s:%i" % address
71 address = "tcp://%s:%i" % address
72 self.address = address
72 self.address = address
73 atexit.register(self._notice_exit)
73 atexit.register(self._notice_exit)
74
74
75 self._running = False
75 self._running = False
76 self._pause = True
76 self._pause = True
77 self.poller = zmq.Poller()
77 self.poller = zmq.Poller()
78
78
79 def _notice_exit(self):
79 def _notice_exit(self):
80 self._exiting = True
80 self._exiting = True
81
81
82 def _create_socket(self):
82 def _create_socket(self):
83 if self.socket is not None:
83 if self.socket is not None:
84 # close previous socket, before opening a new one
84 # close previous socket, before opening a new one
85 self.poller.unregister(self.socket)
85 self.poller.unregister(self.socket)
86 self.socket.close()
86 self.socket.close()
87 self.socket = self.context.socket(zmq.REQ)
87 self.socket = self.context.socket(zmq.REQ)
88 self.socket.linger = 1000
88 self.socket.linger = 1000
89 self.socket.connect(self.address)
89 self.socket.connect(self.address)
90
90
91 self.poller.register(self.socket, zmq.POLLIN)
91 self.poller.register(self.socket, zmq.POLLIN)
92
92
93 def _poll(self, start_time):
93 def _poll(self, start_time):
94 """poll for heartbeat replies until we reach self.time_to_dead.
94 """poll for heartbeat replies until we reach self.time_to_dead.
95
95
96 Ignores interrupts, and returns the result of poll(), which
96 Ignores interrupts, and returns the result of poll(), which
97 will be an empty list if no messages arrived before the timeout,
97 will be an empty list if no messages arrived before the timeout,
98 or the event tuple if there is a message to receive.
98 or the event tuple if there is a message to receive.
99 """
99 """
100
100
101 until_dead = self.time_to_dead - (time.time() - start_time)
101 until_dead = self.time_to_dead - (time.time() - start_time)
102 # ensure poll at least once
102 # ensure poll at least once
103 until_dead = max(until_dead, 1e-3)
103 until_dead = max(until_dead, 1e-3)
104 events = []
104 events = []
105 while True:
105 while True:
106 try:
106 try:
107 events = self.poller.poll(1000 * until_dead)
107 events = self.poller.poll(1000 * until_dead)
108 except ZMQError as e:
108 except ZMQError as e:
109 if e.errno == errno.EINTR:
109 if e.errno == errno.EINTR:
110 # ignore interrupts during heartbeat
110 # ignore interrupts during heartbeat
111 # this may never actually happen
111 # this may never actually happen
112 until_dead = self.time_to_dead - (time.time() - start_time)
112 until_dead = self.time_to_dead - (time.time() - start_time)
113 until_dead = max(until_dead, 1e-3)
113 until_dead = max(until_dead, 1e-3)
114 pass
114 pass
115 else:
115 else:
116 raise
116 raise
117 except Exception:
117 except Exception:
118 if self._exiting:
118 if self._exiting:
119 break
119 break
120 else:
120 else:
121 raise
121 raise
122 else:
122 else:
123 break
123 break
124 return events
124 return events
125
125
126 def run(self):
126 def run(self):
127 """The thread's main activity. Call start() instead."""
127 """The thread's main activity. Call start() instead."""
128 self._create_socket()
128 self._create_socket()
129 self._running = True
129 self._running = True
130 self._beating = True
130 self._beating = True
131
131
132 while self._running:
132 while self._running:
133 if self._pause:
133 if self._pause:
134 # just sleep, and skip the rest of the loop
134 # just sleep, and skip the rest of the loop
135 time.sleep(self.time_to_dead)
135 time.sleep(self.time_to_dead)
136 continue
136 continue
137
137
138 since_last_heartbeat = 0.0
138 since_last_heartbeat = 0.0
139 # io.rprint('Ping from HB channel') # dbg
139 # io.rprint('Ping from HB channel') # dbg
140 # no need to catch EFSM here, because the previous event was
140 # no need to catch EFSM here, because the previous event was
141 # either a recv or connect, which cannot be followed by EFSM
141 # either a recv or connect, which cannot be followed by EFSM
142 self.socket.send(b'ping')
142 self.socket.send(b'ping')
143 request_time = time.time()
143 request_time = time.time()
144 ready = self._poll(request_time)
144 ready = self._poll(request_time)
145 if ready:
145 if ready:
146 self._beating = True
146 self._beating = True
147 # the poll above guarantees we have something to recv
147 # the poll above guarantees we have something to recv
148 self.socket.recv()
148 self.socket.recv()
149 # sleep the remainder of the cycle
149 # sleep the remainder of the cycle
150 remainder = self.time_to_dead - (time.time() - request_time)
150 remainder = self.time_to_dead - (time.time() - request_time)
151 if remainder > 0:
151 if remainder > 0:
152 time.sleep(remainder)
152 time.sleep(remainder)
153 continue
153 continue
154 else:
154 else:
155 # nothing was received within the time limit, signal heart failure
155 # nothing was received within the time limit, signal heart failure
156 self._beating = False
156 self._beating = False
157 since_last_heartbeat = time.time() - request_time
157 since_last_heartbeat = time.time() - request_time
158 self.call_handlers(since_last_heartbeat)
158 self.call_handlers(since_last_heartbeat)
159 # and close/reopen the socket, because the REQ/REP cycle has been broken
159 # and close/reopen the socket, because the REQ/REP cycle has been broken
160 self._create_socket()
160 self._create_socket()
161 continue
161 continue
162
162
163 def pause(self):
163 def pause(self):
164 """Pause the heartbeat."""
164 """Pause the heartbeat."""
165 self._pause = True
165 self._pause = True
166
166
167 def unpause(self):
167 def unpause(self):
168 """Unpause the heartbeat."""
168 """Unpause the heartbeat."""
169 self._pause = False
169 self._pause = False
170
170
171 def is_beating(self):
171 def is_beating(self):
172 """Is the heartbeat running and responsive (and not paused)."""
172 """Is the heartbeat running and responsive (and not paused)."""
173 if self.is_alive() and not self._pause and self._beating:
173 if self.is_alive() and not self._pause and self._beating:
174 return True
174 return True
175 else:
175 else:
176 return False
176 return False
177
177
178 def stop(self):
178 def stop(self):
179 """Stop the channel's event loop and join its thread."""
179 """Stop the channel's event loop and join its thread."""
180 self._running = False
180 self._running = False
181 self.join()
181 self.join()
182 self.close()
182 self.close()
183
183
184 def close(self):
184 def close(self):
185 if self.socket is not None:
185 if self.socket is not None:
186 try:
186 try:
187 self.socket.close(linger=0)
187 self.socket.close(linger=0)
188 except Exception:
188 except Exception:
189 pass
189 pass
190 self.socket = None
190 self.socket = None
191
191
192 def call_handlers(self, since_last_heartbeat):
192 def call_handlers(self, since_last_heartbeat):
193 """This method is called in the ioloop thread when a message arrives.
193 """This method is called in the ioloop thread when a message arrives.
194
194
195 Subclasses should override this method to handle incoming messages.
195 Subclasses should override this method to handle incoming messages.
196 It is important to remember that this method is called in the thread
196 It is important to remember that this method is called in the thread
197 so that some logic must be done to ensure that the application level
197 so that some logic must be done to ensure that the application level
198 handlers are called in the application thread.
198 handlers are called in the application thread.
199 """
199 """
200 pass
200 pass
201
201
202
202
203 HBChannelABC.register(HBChannel)
203 HBChannelABC.register(HBChannel)
@@ -1,598 +1,598 b''
1 """A FrontendWidget that emulates the interface of the console IPython.
1 """A FrontendWidget that emulates the interface of the console IPython.
2
2
3 This supports the additional functionality provided by the IPython kernel.
3 This supports the additional functionality provided by the IPython kernel.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 from collections import namedtuple
9 from collections import namedtuple
10 import os.path
10 import os.path
11 import re
11 import re
12 from subprocess import Popen
12 from subprocess import Popen
13 import sys
13 import sys
14 import time
14 import time
15 from textwrap import dedent
15 from textwrap import dedent
16
16
17 from IPython.external.qt import QtCore, QtGui
17 from IPython.external.qt import QtCore, QtGui
18
18
19 from IPython.core.inputsplitter import IPythonInputSplitter
19 from IPython.core.inputsplitter import IPythonInputSplitter
20 from IPython.core.release import version
20 from IPython.core.release import version
21 from IPython.core.inputtransformer import ipy_prompt
21 from IPython.core.inputtransformer import ipy_prompt
22 from IPython.utils.traitlets import Bool, Unicode
22 from IPython.utils.traitlets import Bool, Unicode
23 from .frontend_widget import FrontendWidget
23 from .frontend_widget import FrontendWidget
24 from . import styles
24 from . import styles
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Constants
27 # Constants
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30 # Default strings to build and display input and output prompts (and separators
30 # Default strings to build and display input and output prompts (and separators
31 # in between)
31 # in between)
32 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
32 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
33 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
33 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
34 default_input_sep = '\n'
34 default_input_sep = '\n'
35 default_output_sep = ''
35 default_output_sep = ''
36 default_output_sep2 = ''
36 default_output_sep2 = ''
37
37
38 # Base path for most payload sources.
38 # Base path for most payload sources.
39 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
39 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
40
40
41 if sys.platform.startswith('win'):
41 if sys.platform.startswith('win'):
42 default_editor = 'notepad'
42 default_editor = 'notepad'
43 else:
43 else:
44 default_editor = ''
44 default_editor = ''
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # IPythonWidget class
47 # IPythonWidget class
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49
49
50 class IPythonWidget(FrontendWidget):
50 class IPythonWidget(FrontendWidget):
51 """ A FrontendWidget for an IPython kernel.
51 """ A FrontendWidget for an IPython kernel.
52 """
52 """
53
53
54 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
54 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
55 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
55 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
56 # settings.
56 # settings.
57 custom_edit = Bool(False)
57 custom_edit = Bool(False)
58 custom_edit_requested = QtCore.Signal(object, object)
58 custom_edit_requested = QtCore.Signal(object, object)
59
59
60 editor = Unicode(default_editor, config=True,
60 editor = Unicode(default_editor, config=True,
61 help="""
61 help="""
62 A command for invoking a system text editor. If the string contains a
62 A command for invoking a system text editor. If the string contains a
63 {filename} format specifier, it will be used. Otherwise, the filename
63 {filename} format specifier, it will be used. Otherwise, the filename
64 will be appended to the end the command.
64 will be appended to the end the command.
65 """)
65 """)
66
66
67 editor_line = Unicode(config=True,
67 editor_line = Unicode(config=True,
68 help="""
68 help="""
69 The editor command to use when a specific line number is requested. The
69 The editor command to use when a specific line number is requested. The
70 string should contain two format specifiers: {line} and {filename}. If
70 string should contain two format specifiers: {line} and {filename}. If
71 this parameter is not specified, the line number option to the %edit
71 this parameter is not specified, the line number option to the %edit
72 magic will be ignored.
72 magic will be ignored.
73 """)
73 """)
74
74
75 style_sheet = Unicode(config=True,
75 style_sheet = Unicode(config=True,
76 help="""
76 help="""
77 A CSS stylesheet. The stylesheet can contain classes for:
77 A CSS stylesheet. The stylesheet can contain classes for:
78 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
78 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
79 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
79 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
80 3. IPython: .error, .in-prompt, .out-prompt, etc
80 3. IPython: .error, .in-prompt, .out-prompt, etc
81 """)
81 """)
82
82
83 syntax_style = Unicode(config=True,
83 syntax_style = Unicode(config=True,
84 help="""
84 help="""
85 If not empty, use this Pygments style for syntax highlighting.
85 If not empty, use this Pygments style for syntax highlighting.
86 Otherwise, the style sheet is queried for Pygments style
86 Otherwise, the style sheet is queried for Pygments style
87 information.
87 information.
88 """)
88 """)
89
89
90 # Prompts.
90 # Prompts.
91 in_prompt = Unicode(default_in_prompt, config=True)
91 in_prompt = Unicode(default_in_prompt, config=True)
92 out_prompt = Unicode(default_out_prompt, config=True)
92 out_prompt = Unicode(default_out_prompt, config=True)
93 input_sep = Unicode(default_input_sep, config=True)
93 input_sep = Unicode(default_input_sep, config=True)
94 output_sep = Unicode(default_output_sep, config=True)
94 output_sep = Unicode(default_output_sep, config=True)
95 output_sep2 = Unicode(default_output_sep2, config=True)
95 output_sep2 = Unicode(default_output_sep2, config=True)
96
96
97 # FrontendWidget protected class variables.
97 # FrontendWidget protected class variables.
98 _input_splitter_class = IPythonInputSplitter
98 _input_splitter_class = IPythonInputSplitter
99 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
99 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
100 logical_line_transforms=[],
100 logical_line_transforms=[],
101 python_line_transforms=[],
101 python_line_transforms=[],
102 )
102 )
103
103
104 # IPythonWidget protected class variables.
104 # IPythonWidget protected class variables.
105 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
105 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
106 _payload_source_edit = 'edit'
106 _payload_source_edit = 'edit'
107 _payload_source_exit = 'ask_exit'
107 _payload_source_exit = 'ask_exit'
108 _payload_source_next_input = 'set_next_input'
108 _payload_source_next_input = 'set_next_input'
109 _payload_source_page = 'page'
109 _payload_source_page = 'page'
110 _retrying_history_request = False
110 _retrying_history_request = False
111 _starting = False
111 _starting = False
112
112
113 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
114 # 'object' interface
114 # 'object' interface
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116
116
117 def __init__(self, *args, **kw):
117 def __init__(self, *args, **kw):
118 super(IPythonWidget, self).__init__(*args, **kw)
118 super(IPythonWidget, self).__init__(*args, **kw)
119
119
120 # IPythonWidget protected variables.
120 # IPythonWidget protected variables.
121 self._payload_handlers = {
121 self._payload_handlers = {
122 self._payload_source_edit : self._handle_payload_edit,
122 self._payload_source_edit : self._handle_payload_edit,
123 self._payload_source_exit : self._handle_payload_exit,
123 self._payload_source_exit : self._handle_payload_exit,
124 self._payload_source_page : self._handle_payload_page,
124 self._payload_source_page : self._handle_payload_page,
125 self._payload_source_next_input : self._handle_payload_next_input }
125 self._payload_source_next_input : self._handle_payload_next_input }
126 self._previous_prompt_obj = None
126 self._previous_prompt_obj = None
127 self._keep_kernel_on_exit = None
127 self._keep_kernel_on_exit = None
128
128
129 # Initialize widget styling.
129 # Initialize widget styling.
130 if self.style_sheet:
130 if self.style_sheet:
131 self._style_sheet_changed()
131 self._style_sheet_changed()
132 self._syntax_style_changed()
132 self._syntax_style_changed()
133 else:
133 else:
134 self.set_default_style()
134 self.set_default_style()
135
135
136 self._guiref_loaded = False
136 self._guiref_loaded = False
137
137
138 #---------------------------------------------------------------------------
138 #---------------------------------------------------------------------------
139 # 'BaseFrontendMixin' abstract interface
139 # 'BaseFrontendMixin' abstract interface
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 content = rep['content']
149 content = rep['content']
150 matches = content['matches']
150 matches = content['matches']
151 start = content['cursor_start']
151 start = content['cursor_start']
152 end = content['cursor_end']
152 end = content['cursor_end']
153
153
154 start = max(start, 0)
154 start = max(start, 0)
155 end = max(end, start)
155 end = max(end, start)
156
156
157 # Move the control's cursor to the desired end point
157 # Move the control's cursor to the desired end point
158 cursor_pos = self._get_input_buffer_cursor_pos()
158 cursor_pos = self._get_input_buffer_cursor_pos()
159 if end < cursor_pos:
159 if end < cursor_pos:
160 cursor.movePosition(QtGui.QTextCursor.Left,
160 cursor.movePosition(QtGui.QTextCursor.Left,
161 n=(cursor_pos - end))
161 n=(cursor_pos - end))
162 elif end > cursor_pos:
162 elif end > cursor_pos:
163 cursor.movePosition(QtGui.QTextCursor.Right,
163 cursor.movePosition(QtGui.QTextCursor.Right,
164 n=(end - cursor_pos))
164 n=(end - cursor_pos))
165 # This line actually applies the move to control's cursor
165 # This line actually applies the move to control's cursor
166 self._control.setTextCursor(cursor)
166 self._control.setTextCursor(cursor)
167
167
168 offset = end - start
168 offset = end - start
169 # Move the local cursor object to the start of the match and
169 # Move the local cursor object to the start of the match and
170 # complete.
170 # complete.
171 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
171 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
172 self._complete_with_items(cursor, matches)
172 self._complete_with_items(cursor, matches)
173
173
174 def _handle_execute_reply(self, msg):
174 def _handle_execute_reply(self, msg):
175 """ Reimplemented to support prompt requests.
175 """ Reimplemented to support prompt requests.
176 """
176 """
177 msg_id = msg['parent_header'].get('msg_id')
177 msg_id = msg['parent_header'].get('msg_id')
178 info = self._request_info['execute'].get(msg_id)
178 info = self._request_info['execute'].get(msg_id)
179 if info and info.kind == 'prompt':
179 if info and info.kind == 'prompt':
180 content = msg['content']
180 content = msg['content']
181 if content['status'] == 'aborted':
181 if content['status'] == 'aborted':
182 self._show_interpreter_prompt()
182 self._show_interpreter_prompt()
183 else:
183 else:
184 number = content['execution_count'] + 1
184 number = content['execution_count'] + 1
185 self._show_interpreter_prompt(number)
185 self._show_interpreter_prompt(number)
186 self._request_info['execute'].pop(msg_id)
186 self._request_info['execute'].pop(msg_id)
187 else:
187 else:
188 super(IPythonWidget, self)._handle_execute_reply(msg)
188 super(IPythonWidget, self)._handle_execute_reply(msg)
189
189
190 def _handle_history_reply(self, msg):
190 def _handle_history_reply(self, msg):
191 """ Implemented to handle history tail replies, which are only supported
191 """ Implemented to handle history tail replies, which are only supported
192 by the IPython kernel.
192 by the IPython kernel.
193 """
193 """
194 content = msg['content']
194 content = msg['content']
195 if 'history' not in content:
195 if 'history' not in content:
196 self.log.error("History request failed: %r"%content)
196 self.log.error("History request failed: %r"%content)
197 if content.get('status', '') == 'aborted' and \
197 if content.get('status', '') == 'aborted' and \
198 not self._retrying_history_request:
198 not self._retrying_history_request:
199 # a *different* action caused this request to be aborted, so
199 # a *different* action caused this request to be aborted, so
200 # we should try again.
200 # we should try again.
201 self.log.error("Retrying aborted history request")
201 self.log.error("Retrying aborted history request")
202 # prevent multiple retries of aborted requests:
202 # prevent multiple retries of aborted requests:
203 self._retrying_history_request = True
203 self._retrying_history_request = True
204 # wait out the kernel's queue flush, which is currently timed at 0.1s
204 # wait out the kernel's queue flush, which is currently timed at 0.1s
205 time.sleep(0.25)
205 time.sleep(0.25)
206 self.kernel_client.history(hist_access_type='tail',n=1000)
206 self.kernel_client.history(hist_access_type='tail',n=1000)
207 else:
207 else:
208 self._retrying_history_request = False
208 self._retrying_history_request = False
209 return
209 return
210 # reset retry flag
210 # reset retry flag
211 self._retrying_history_request = False
211 self._retrying_history_request = False
212 history_items = content['history']
212 history_items = content['history']
213 self.log.debug("Received history reply with %i entries", len(history_items))
213 self.log.debug("Received history reply with %i entries", len(history_items))
214 items = []
214 items = []
215 last_cell = u""
215 last_cell = u""
216 for _, _, cell in history_items:
216 for _, _, cell in history_items:
217 cell = cell.rstrip()
217 cell = cell.rstrip()
218 if cell != last_cell:
218 if cell != last_cell:
219 items.append(cell)
219 items.append(cell)
220 last_cell = cell
220 last_cell = cell
221 self._set_history(items)
221 self._set_history(items)
222
222
223 def _insert_other_input(self, cursor, content):
223 def _insert_other_input(self, cursor, content):
224 """Insert function for input from other frontends"""
224 """Insert function for input from other frontends"""
225 cursor.beginEditBlock()
225 cursor.beginEditBlock()
226 start = cursor.position()
226 start = cursor.position()
227 n = content.get('execution_count', 0)
227 n = content.get('execution_count', 0)
228 cursor.insertText('\n')
228 cursor.insertText('\n')
229 self._insert_html(cursor, self._make_in_prompt(n))
229 self._insert_html(cursor, self._make_in_prompt(n))
230 cursor.insertText(content['code'])
230 cursor.insertText(content['code'])
231 self._highlighter.rehighlightBlock(cursor.block())
231 self._highlighter.rehighlightBlock(cursor.block())
232 cursor.endEditBlock()
232 cursor.endEditBlock()
233
233
234 def _handle_execute_input(self, msg):
234 def _handle_execute_input(self, msg):
235 """Handle an execute_input message"""
235 """Handle an execute_input message"""
236 self.log.debug("execute_input: %s", msg.get('content', ''))
236 self.log.debug("execute_input: %s", msg.get('content', ''))
237 if self.include_output(msg):
237 if self.include_output(msg):
238 self._append_custom(self._insert_other_input, msg['content'], before_prompt=True)
238 self._append_custom(self._insert_other_input, msg['content'], before_prompt=True)
239
239
240
240
241 def _handle_execute_result(self, msg):
241 def _handle_execute_result(self, msg):
242 """ Reimplemented for IPython-style "display hook".
242 """ Reimplemented for IPython-style "display hook".
243 """
243 """
244 self.log.debug("execute_result: %s", msg.get('content', ''))
244 self.log.debug("execute_result: %s", msg.get('content', ''))
245 if self.include_output(msg):
245 if self.include_output(msg):
246 self.flush_clearoutput()
246 self.flush_clearoutput()
247 content = msg['content']
247 content = msg['content']
248 prompt_number = content.get('execution_count', 0)
248 prompt_number = content.get('execution_count', 0)
249 data = content['data']
249 data = content['data']
250 if 'text/plain' in data:
250 if 'text/plain' in data:
251 self._append_plain_text(self.output_sep, True)
251 self._append_plain_text(self.output_sep, True)
252 self._append_html(self._make_out_prompt(prompt_number), True)
252 self._append_html(self._make_out_prompt(prompt_number), True)
253 text = data['text/plain']
253 text = data['text/plain']
254 # If the repr is multiline, make sure we start on a new line,
254 # If the repr is multiline, make sure we start on a new line,
255 # so that its lines are aligned.
255 # so that its lines are aligned.
256 if "\n" in text and not self.output_sep.endswith("\n"):
256 if "\n" in text and not self.output_sep.endswith("\n"):
257 self._append_plain_text('\n', True)
257 self._append_plain_text('\n', True)
258 self._append_plain_text(text + self.output_sep2, True)
258 self._append_plain_text(text + self.output_sep2, True)
259
259
260 def _handle_display_data(self, msg):
260 def _handle_display_data(self, msg):
261 """ The base handler for the ``display_data`` message.
261 """ The base handler for the ``display_data`` message.
262 """
262 """
263 self.log.debug("display: %s", msg.get('content', ''))
263 self.log.debug("display: %s", msg.get('content', ''))
264 # For now, we don't display data from other frontends, but we
264 # For now, we don't display data from other frontends, but we
265 # eventually will as this allows all frontends to monitor the display
265 # eventually will as this allows all frontends to monitor the display
266 # data. But we need to figure out how to handle this in the GUI.
266 # data. But we need to figure out how to handle this in the GUI.
267 if self.include_output(msg):
267 if self.include_output(msg):
268 self.flush_clearoutput()
268 self.flush_clearoutput()
269 data = msg['content']['data']
269 data = msg['content']['data']
270 metadata = msg['content']['metadata']
270 metadata = msg['content']['metadata']
271 # In the regular IPythonWidget, we simply print the plain text
271 # In the regular IPythonWidget, we simply print the plain text
272 # representation.
272 # representation.
273 if 'text/plain' in data:
273 if 'text/plain' in data:
274 text = data['text/plain']
274 text = data['text/plain']
275 self._append_plain_text(text, True)
275 self._append_plain_text(text, True)
276 # This newline seems to be needed for text and html output.
276 # This newline seems to be needed for text and html output.
277 self._append_plain_text(u'\n', True)
277 self._append_plain_text(u'\n', True)
278
278
279 def _handle_kernel_info_reply(self, rep):
279 def _handle_kernel_info_reply(self, rep):
280 """Handle kernel info replies."""
280 """Handle kernel info replies."""
281 content = rep['content']
281 content = rep['content']
282 if not self._guiref_loaded:
282 if not self._guiref_loaded:
283 if content.get('language') == 'python':
283 if content.get('implementation') == 'ipython':
284 self._load_guiref_magic()
284 self._load_guiref_magic()
285 self._guiref_loaded = True
285 self._guiref_loaded = True
286
286
287 self.kernel_banner = content.get('banner', '')
287 self.kernel_banner = content.get('banner', '')
288 if self._starting:
288 if self._starting:
289 # finish handling started channels
289 # finish handling started channels
290 self._starting = False
290 self._starting = False
291 super(IPythonWidget, self)._started_channels()
291 super(IPythonWidget, self)._started_channels()
292
292
293 def _started_channels(self):
293 def _started_channels(self):
294 """Reimplemented to make a history request and load %guiref."""
294 """Reimplemented to make a history request and load %guiref."""
295 self._starting = True
295 self._starting = True
296 # The reply will trigger %guiref load provided language=='python'
296 # The reply will trigger %guiref load provided language=='python'
297 self.kernel_client.kernel_info()
297 self.kernel_client.kernel_info()
298
298
299 self.kernel_client.history(hist_access_type='tail', n=1000)
299 self.kernel_client.history(hist_access_type='tail', n=1000)
300
300
301 def _load_guiref_magic(self):
301 def _load_guiref_magic(self):
302 """Load %guiref magic."""
302 """Load %guiref magic."""
303 self.kernel_client.execute('\n'.join([
303 self.kernel_client.execute('\n'.join([
304 "try:",
304 "try:",
305 " _usage",
305 " _usage",
306 "except:",
306 "except:",
307 " from IPython.core import usage as _usage",
307 " from IPython.core import usage as _usage",
308 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
308 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
309 " del _usage",
309 " del _usage",
310 ]), silent=True)
310 ]), silent=True)
311
311
312 #---------------------------------------------------------------------------
312 #---------------------------------------------------------------------------
313 # 'ConsoleWidget' public interface
313 # 'ConsoleWidget' public interface
314 #---------------------------------------------------------------------------
314 #---------------------------------------------------------------------------
315
315
316 #---------------------------------------------------------------------------
316 #---------------------------------------------------------------------------
317 # 'FrontendWidget' public interface
317 # 'FrontendWidget' public interface
318 #---------------------------------------------------------------------------
318 #---------------------------------------------------------------------------
319
319
320 def execute_file(self, path, hidden=False):
320 def execute_file(self, path, hidden=False):
321 """ Reimplemented to use the 'run' magic.
321 """ Reimplemented to use the 'run' magic.
322 """
322 """
323 # Use forward slashes on Windows to avoid escaping each separator.
323 # Use forward slashes on Windows to avoid escaping each separator.
324 if sys.platform == 'win32':
324 if sys.platform == 'win32':
325 path = os.path.normpath(path).replace('\\', '/')
325 path = os.path.normpath(path).replace('\\', '/')
326
326
327 # Perhaps we should not be using %run directly, but while we
327 # Perhaps we should not be using %run directly, but while we
328 # are, it is necessary to quote or escape filenames containing spaces
328 # are, it is necessary to quote or escape filenames containing spaces
329 # or quotes.
329 # or quotes.
330
330
331 # In earlier code here, to minimize escaping, we sometimes quoted the
331 # In earlier code here, to minimize escaping, we sometimes quoted the
332 # filename with single quotes. But to do this, this code must be
332 # filename with single quotes. But to do this, this code must be
333 # platform-aware, because run uses shlex rather than python string
333 # platform-aware, because run uses shlex rather than python string
334 # parsing, so that:
334 # parsing, so that:
335 # * In Win: single quotes can be used in the filename without quoting,
335 # * In Win: single quotes can be used in the filename without quoting,
336 # and we cannot use single quotes to quote the filename.
336 # and we cannot use single quotes to quote the filename.
337 # * In *nix: we can escape double quotes in a double quoted filename,
337 # * In *nix: we can escape double quotes in a double quoted filename,
338 # but can't escape single quotes in a single quoted filename.
338 # but can't escape single quotes in a single quoted filename.
339
339
340 # So to keep this code non-platform-specific and simple, we now only
340 # So to keep this code non-platform-specific and simple, we now only
341 # use double quotes to quote filenames, and escape when needed:
341 # use double quotes to quote filenames, and escape when needed:
342 if ' ' in path or "'" in path or '"' in path:
342 if ' ' in path or "'" in path or '"' in path:
343 path = '"%s"' % path.replace('"', '\\"')
343 path = '"%s"' % path.replace('"', '\\"')
344 self.execute('%%run %s' % path, hidden=hidden)
344 self.execute('%%run %s' % path, hidden=hidden)
345
345
346 #---------------------------------------------------------------------------
346 #---------------------------------------------------------------------------
347 # 'FrontendWidget' protected interface
347 # 'FrontendWidget' protected interface
348 #---------------------------------------------------------------------------
348 #---------------------------------------------------------------------------
349
349
350 def _process_execute_error(self, msg):
350 def _process_execute_error(self, msg):
351 """ Reimplemented for IPython-style traceback formatting.
351 """ Reimplemented for IPython-style traceback formatting.
352 """
352 """
353 content = msg['content']
353 content = msg['content']
354 traceback = '\n'.join(content['traceback']) + '\n'
354 traceback = '\n'.join(content['traceback']) + '\n'
355 if False:
355 if False:
356 # FIXME: For now, tracebacks come as plain text, so we can't use
356 # FIXME: For now, tracebacks come as plain text, so we can't use
357 # the html renderer yet. Once we refactor ultratb to produce
357 # the html renderer yet. Once we refactor ultratb to produce
358 # properly styled tracebacks, this branch should be the default
358 # properly styled tracebacks, this branch should be the default
359 traceback = traceback.replace(' ', '&nbsp;')
359 traceback = traceback.replace(' ', '&nbsp;')
360 traceback = traceback.replace('\n', '<br/>')
360 traceback = traceback.replace('\n', '<br/>')
361
361
362 ename = content['ename']
362 ename = content['ename']
363 ename_styled = '<span class="error">%s</span>' % ename
363 ename_styled = '<span class="error">%s</span>' % ename
364 traceback = traceback.replace(ename, ename_styled)
364 traceback = traceback.replace(ename, ename_styled)
365
365
366 self._append_html(traceback)
366 self._append_html(traceback)
367 else:
367 else:
368 # This is the fallback for now, using plain text with ansi escapes
368 # This is the fallback for now, using plain text with ansi escapes
369 self._append_plain_text(traceback)
369 self._append_plain_text(traceback)
370
370
371 def _process_execute_payload(self, item):
371 def _process_execute_payload(self, item):
372 """ Reimplemented to dispatch payloads to handler methods.
372 """ Reimplemented to dispatch payloads to handler methods.
373 """
373 """
374 handler = self._payload_handlers.get(item['source'])
374 handler = self._payload_handlers.get(item['source'])
375 if handler is None:
375 if handler is None:
376 # We have no handler for this type of payload, simply ignore it
376 # We have no handler for this type of payload, simply ignore it
377 return False
377 return False
378 else:
378 else:
379 handler(item)
379 handler(item)
380 return True
380 return True
381
381
382 def _show_interpreter_prompt(self, number=None):
382 def _show_interpreter_prompt(self, number=None):
383 """ Reimplemented for IPython-style prompts.
383 """ Reimplemented for IPython-style prompts.
384 """
384 """
385 # If a number was not specified, make a prompt number request.
385 # If a number was not specified, make a prompt number request.
386 if number is None:
386 if number is None:
387 msg_id = self.kernel_client.execute('', silent=True)
387 msg_id = self.kernel_client.execute('', silent=True)
388 info = self._ExecutionRequest(msg_id, 'prompt')
388 info = self._ExecutionRequest(msg_id, 'prompt')
389 self._request_info['execute'][msg_id] = info
389 self._request_info['execute'][msg_id] = info
390 return
390 return
391
391
392 # Show a new prompt and save information about it so that it can be
392 # Show a new prompt and save information about it so that it can be
393 # updated later if the prompt number turns out to be wrong.
393 # updated later if the prompt number turns out to be wrong.
394 self._prompt_sep = self.input_sep
394 self._prompt_sep = self.input_sep
395 self._show_prompt(self._make_in_prompt(number), html=True)
395 self._show_prompt(self._make_in_prompt(number), html=True)
396 block = self._control.document().lastBlock()
396 block = self._control.document().lastBlock()
397 length = len(self._prompt)
397 length = len(self._prompt)
398 self._previous_prompt_obj = self._PromptBlock(block, length, number)
398 self._previous_prompt_obj = self._PromptBlock(block, length, number)
399
399
400 # Update continuation prompt to reflect (possibly) new prompt length.
400 # Update continuation prompt to reflect (possibly) new prompt length.
401 self._set_continuation_prompt(
401 self._set_continuation_prompt(
402 self._make_continuation_prompt(self._prompt), html=True)
402 self._make_continuation_prompt(self._prompt), html=True)
403
403
404 def _show_interpreter_prompt_for_reply(self, msg):
404 def _show_interpreter_prompt_for_reply(self, msg):
405 """ Reimplemented for IPython-style prompts.
405 """ Reimplemented for IPython-style prompts.
406 """
406 """
407 # Update the old prompt number if necessary.
407 # Update the old prompt number if necessary.
408 content = msg['content']
408 content = msg['content']
409 # abort replies do not have any keys:
409 # abort replies do not have any keys:
410 if content['status'] == 'aborted':
410 if content['status'] == 'aborted':
411 if self._previous_prompt_obj:
411 if self._previous_prompt_obj:
412 previous_prompt_number = self._previous_prompt_obj.number
412 previous_prompt_number = self._previous_prompt_obj.number
413 else:
413 else:
414 previous_prompt_number = 0
414 previous_prompt_number = 0
415 else:
415 else:
416 previous_prompt_number = content['execution_count']
416 previous_prompt_number = content['execution_count']
417 if self._previous_prompt_obj and \
417 if self._previous_prompt_obj and \
418 self._previous_prompt_obj.number != previous_prompt_number:
418 self._previous_prompt_obj.number != previous_prompt_number:
419 block = self._previous_prompt_obj.block
419 block = self._previous_prompt_obj.block
420
420
421 # Make sure the prompt block has not been erased.
421 # Make sure the prompt block has not been erased.
422 if block.isValid() and block.text():
422 if block.isValid() and block.text():
423
423
424 # Remove the old prompt and insert a new prompt.
424 # Remove the old prompt and insert a new prompt.
425 cursor = QtGui.QTextCursor(block)
425 cursor = QtGui.QTextCursor(block)
426 cursor.movePosition(QtGui.QTextCursor.Right,
426 cursor.movePosition(QtGui.QTextCursor.Right,
427 QtGui.QTextCursor.KeepAnchor,
427 QtGui.QTextCursor.KeepAnchor,
428 self._previous_prompt_obj.length)
428 self._previous_prompt_obj.length)
429 prompt = self._make_in_prompt(previous_prompt_number)
429 prompt = self._make_in_prompt(previous_prompt_number)
430 self._prompt = self._insert_html_fetching_plain_text(
430 self._prompt = self._insert_html_fetching_plain_text(
431 cursor, prompt)
431 cursor, prompt)
432
432
433 # When the HTML is inserted, Qt blows away the syntax
433 # When the HTML is inserted, Qt blows away the syntax
434 # highlighting for the line, so we need to rehighlight it.
434 # highlighting for the line, so we need to rehighlight it.
435 self._highlighter.rehighlightBlock(cursor.block())
435 self._highlighter.rehighlightBlock(cursor.block())
436
436
437 self._previous_prompt_obj = None
437 self._previous_prompt_obj = None
438
438
439 # Show a new prompt with the kernel's estimated prompt number.
439 # Show a new prompt with the kernel's estimated prompt number.
440 self._show_interpreter_prompt(previous_prompt_number + 1)
440 self._show_interpreter_prompt(previous_prompt_number + 1)
441
441
442 #---------------------------------------------------------------------------
442 #---------------------------------------------------------------------------
443 # 'IPythonWidget' interface
443 # 'IPythonWidget' interface
444 #---------------------------------------------------------------------------
444 #---------------------------------------------------------------------------
445
445
446 def set_default_style(self, colors='lightbg'):
446 def set_default_style(self, colors='lightbg'):
447 """ Sets the widget style to the class defaults.
447 """ Sets the widget style to the class defaults.
448
448
449 Parameters
449 Parameters
450 ----------
450 ----------
451 colors : str, optional (default lightbg)
451 colors : str, optional (default lightbg)
452 Whether to use the default IPython light background or dark
452 Whether to use the default IPython light background or dark
453 background or B&W style.
453 background or B&W style.
454 """
454 """
455 colors = colors.lower()
455 colors = colors.lower()
456 if colors=='lightbg':
456 if colors=='lightbg':
457 self.style_sheet = styles.default_light_style_sheet
457 self.style_sheet = styles.default_light_style_sheet
458 self.syntax_style = styles.default_light_syntax_style
458 self.syntax_style = styles.default_light_syntax_style
459 elif colors=='linux':
459 elif colors=='linux':
460 self.style_sheet = styles.default_dark_style_sheet
460 self.style_sheet = styles.default_dark_style_sheet
461 self.syntax_style = styles.default_dark_syntax_style
461 self.syntax_style = styles.default_dark_syntax_style
462 elif colors=='nocolor':
462 elif colors=='nocolor':
463 self.style_sheet = styles.default_bw_style_sheet
463 self.style_sheet = styles.default_bw_style_sheet
464 self.syntax_style = styles.default_bw_syntax_style
464 self.syntax_style = styles.default_bw_syntax_style
465 else:
465 else:
466 raise KeyError("No such color scheme: %s"%colors)
466 raise KeyError("No such color scheme: %s"%colors)
467
467
468 #---------------------------------------------------------------------------
468 #---------------------------------------------------------------------------
469 # 'IPythonWidget' protected interface
469 # 'IPythonWidget' protected interface
470 #---------------------------------------------------------------------------
470 #---------------------------------------------------------------------------
471
471
472 def _edit(self, filename, line=None):
472 def _edit(self, filename, line=None):
473 """ Opens a Python script for editing.
473 """ Opens a Python script for editing.
474
474
475 Parameters
475 Parameters
476 ----------
476 ----------
477 filename : str
477 filename : str
478 A path to a local system file.
478 A path to a local system file.
479
479
480 line : int, optional
480 line : int, optional
481 A line of interest in the file.
481 A line of interest in the file.
482 """
482 """
483 if self.custom_edit:
483 if self.custom_edit:
484 self.custom_edit_requested.emit(filename, line)
484 self.custom_edit_requested.emit(filename, line)
485 elif not self.editor:
485 elif not self.editor:
486 self._append_plain_text('No default editor available.\n'
486 self._append_plain_text('No default editor available.\n'
487 'Specify a GUI text editor in the `IPythonWidget.editor` '
487 'Specify a GUI text editor in the `IPythonWidget.editor` '
488 'configurable to enable the %edit magic')
488 'configurable to enable the %edit magic')
489 else:
489 else:
490 try:
490 try:
491 filename = '"%s"' % filename
491 filename = '"%s"' % filename
492 if line and self.editor_line:
492 if line and self.editor_line:
493 command = self.editor_line.format(filename=filename,
493 command = self.editor_line.format(filename=filename,
494 line=line)
494 line=line)
495 else:
495 else:
496 try:
496 try:
497 command = self.editor.format()
497 command = self.editor.format()
498 except KeyError:
498 except KeyError:
499 command = self.editor.format(filename=filename)
499 command = self.editor.format(filename=filename)
500 else:
500 else:
501 command += ' ' + filename
501 command += ' ' + filename
502 except KeyError:
502 except KeyError:
503 self._append_plain_text('Invalid editor command.\n')
503 self._append_plain_text('Invalid editor command.\n')
504 else:
504 else:
505 try:
505 try:
506 Popen(command, shell=True)
506 Popen(command, shell=True)
507 except OSError:
507 except OSError:
508 msg = 'Opening editor with command "%s" failed.\n'
508 msg = 'Opening editor with command "%s" failed.\n'
509 self._append_plain_text(msg % command)
509 self._append_plain_text(msg % command)
510
510
511 def _make_in_prompt(self, number):
511 def _make_in_prompt(self, number):
512 """ Given a prompt number, returns an HTML In prompt.
512 """ Given a prompt number, returns an HTML In prompt.
513 """
513 """
514 try:
514 try:
515 body = self.in_prompt % number
515 body = self.in_prompt % number
516 except TypeError:
516 except TypeError:
517 # allow in_prompt to leave out number, e.g. '>>> '
517 # allow in_prompt to leave out number, e.g. '>>> '
518 from xml.sax.saxutils import escape
518 from xml.sax.saxutils import escape
519 body = escape(self.in_prompt)
519 body = escape(self.in_prompt)
520 return '<span class="in-prompt">%s</span>' % body
520 return '<span class="in-prompt">%s</span>' % body
521
521
522 def _make_continuation_prompt(self, prompt):
522 def _make_continuation_prompt(self, prompt):
523 """ Given a plain text version of an In prompt, returns an HTML
523 """ Given a plain text version of an In prompt, returns an HTML
524 continuation prompt.
524 continuation prompt.
525 """
525 """
526 end_chars = '...: '
526 end_chars = '...: '
527 space_count = len(prompt.lstrip('\n')) - len(end_chars)
527 space_count = len(prompt.lstrip('\n')) - len(end_chars)
528 body = '&nbsp;' * space_count + end_chars
528 body = '&nbsp;' * space_count + end_chars
529 return '<span class="in-prompt">%s</span>' % body
529 return '<span class="in-prompt">%s</span>' % body
530
530
531 def _make_out_prompt(self, number):
531 def _make_out_prompt(self, number):
532 """ Given a prompt number, returns an HTML Out prompt.
532 """ Given a prompt number, returns an HTML Out prompt.
533 """
533 """
534 try:
534 try:
535 body = self.out_prompt % number
535 body = self.out_prompt % number
536 except TypeError:
536 except TypeError:
537 # allow out_prompt to leave out number, e.g. '<<< '
537 # allow out_prompt to leave out number, e.g. '<<< '
538 from xml.sax.saxutils import escape
538 from xml.sax.saxutils import escape
539 body = escape(self.out_prompt)
539 body = escape(self.out_prompt)
540 return '<span class="out-prompt">%s</span>' % body
540 return '<span class="out-prompt">%s</span>' % body
541
541
542 #------ Payload handlers --------------------------------------------------
542 #------ Payload handlers --------------------------------------------------
543
543
544 # Payload handlers with a generic interface: each takes the opaque payload
544 # Payload handlers with a generic interface: each takes the opaque payload
545 # dict, unpacks it and calls the underlying functions with the necessary
545 # dict, unpacks it and calls the underlying functions with the necessary
546 # arguments.
546 # arguments.
547
547
548 def _handle_payload_edit(self, item):
548 def _handle_payload_edit(self, item):
549 self._edit(item['filename'], item['line_number'])
549 self._edit(item['filename'], item['line_number'])
550
550
551 def _handle_payload_exit(self, item):
551 def _handle_payload_exit(self, item):
552 self._keep_kernel_on_exit = item['keepkernel']
552 self._keep_kernel_on_exit = item['keepkernel']
553 self.exit_requested.emit(self)
553 self.exit_requested.emit(self)
554
554
555 def _handle_payload_next_input(self, item):
555 def _handle_payload_next_input(self, item):
556 self.input_buffer = item['text']
556 self.input_buffer = item['text']
557
557
558 def _handle_payload_page(self, item):
558 def _handle_payload_page(self, item):
559 # Since the plain text widget supports only a very small subset of HTML
559 # Since the plain text widget supports only a very small subset of HTML
560 # and we have no control over the HTML source, we only page HTML
560 # and we have no control over the HTML source, we only page HTML
561 # payloads in the rich text widget.
561 # payloads in the rich text widget.
562 data = item['data']
562 data = item['data']
563 if 'text/html' in data and self.kind == 'rich':
563 if 'text/html' in data and self.kind == 'rich':
564 self._page(data['text/html'], html=True)
564 self._page(data['text/html'], html=True)
565 else:
565 else:
566 self._page(data['text/plain'], html=False)
566 self._page(data['text/plain'], html=False)
567
567
568 #------ Trait change handlers --------------------------------------------
568 #------ Trait change handlers --------------------------------------------
569
569
570 def _style_sheet_changed(self):
570 def _style_sheet_changed(self):
571 """ Set the style sheets of the underlying widgets.
571 """ Set the style sheets of the underlying widgets.
572 """
572 """
573 self.setStyleSheet(self.style_sheet)
573 self.setStyleSheet(self.style_sheet)
574 if self._control is not None:
574 if self._control is not None:
575 self._control.document().setDefaultStyleSheet(self.style_sheet)
575 self._control.document().setDefaultStyleSheet(self.style_sheet)
576 bg_color = self._control.palette().window().color()
576 bg_color = self._control.palette().window().color()
577 self._ansi_processor.set_background_color(bg_color)
577 self._ansi_processor.set_background_color(bg_color)
578
578
579 if self._page_control is not None:
579 if self._page_control is not None:
580 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
580 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
581
581
582
582
583
583
584 def _syntax_style_changed(self):
584 def _syntax_style_changed(self):
585 """ Set the style for the syntax highlighter.
585 """ Set the style for the syntax highlighter.
586 """
586 """
587 if self._highlighter is None:
587 if self._highlighter is None:
588 # ignore premature calls
588 # ignore premature calls
589 return
589 return
590 if self.syntax_style:
590 if self.syntax_style:
591 self._highlighter.set_style(self.syntax_style)
591 self._highlighter.set_style(self.syntax_style)
592 else:
592 else:
593 self._highlighter.set_style_sheet(self.style_sheet)
593 self._highlighter.set_style_sheet(self.style_sheet)
594
594
595 #------ Trait default initializers -----------------------------------------
595 #------ Trait default initializers -----------------------------------------
596
596
597 def _banner_default(self):
597 def _banner_default(self):
598 return "IPython QtConsole {version}\n".format(version=version)
598 return "IPython QtConsole {version}\n".format(version=version)
@@ -1,213 +1,213 b''
1 """MagicHelper - dockable widget showing magic commands for the MainWindow
1 """MagicHelper - dockable widget showing magic commands for the MainWindow
2 """
2 """
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Imports
8 # Imports
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 # stdlib imports
11 # stdlib imports
12 import json
12 import json
13 import re
13 import re
14 import sys
14 import sys
15
15
16 # System library imports
16 # System library imports
17 from IPython.external.qt import QtGui,QtCore
17 from IPython.external.qt import QtGui,QtCore
18
18
19 from IPython.core.magic import magic_escapes
19 from IPython.core.magic import magic_escapes
20
20
21 class MagicHelper(QtGui.QDockWidget):
21 class MagicHelper(QtGui.QDockWidget):
22 """MagicHelper - dockable widget for convenient search and running of
22 """MagicHelper - dockable widget for convenient search and running of
23 magic command for IPython QtConsole.
23 magic command for IPython QtConsole.
24 """
24 """
25
25
26 #---------------------------------------------------------------------------
26 #---------------------------------------------------------------------------
27 # signals
27 # signals
28 #---------------------------------------------------------------------------
28 #---------------------------------------------------------------------------
29
29
30 pasteRequested = QtCore.Signal(str, name = 'pasteRequested')
30 pasteRequested = QtCore.Signal(str, name = 'pasteRequested')
31 """This signal is emitted when user wants to paste selected magic
31 """This signal is emitted when user wants to paste selected magic
32 command into the command line.
32 command into the command line.
33 """
33 """
34
34
35 runRequested = QtCore.Signal(str, name = 'runRequested')
35 runRequested = QtCore.Signal(str, name = 'runRequested')
36 """This signal is emitted when user wants to execute selected magic command
36 """This signal is emitted when user wants to execute selected magic command
37 """
37 """
38
38
39 readyForUpdate = QtCore.Signal(name = 'readyForUpdate')
39 readyForUpdate = QtCore.Signal(name = 'readyForUpdate')
40 """This signal is emitted when MagicHelper is ready to be populated.
40 """This signal is emitted when MagicHelper is ready to be populated.
41 Since kernel querying mechanisms are out of scope of this class,
41 Since kernel querying mechanisms are out of scope of this class,
42 it expects its owner to invoke MagicHelper.populate_magic_helper()
42 it expects its owner to invoke MagicHelper.populate_magic_helper()
43 as a reaction on this event.
43 as a reaction on this event.
44 """
44 """
45
45
46 #---------------------------------------------------------------------------
46 #---------------------------------------------------------------------------
47 # constructor
47 # constructor
48 #---------------------------------------------------------------------------
48 #---------------------------------------------------------------------------
49
49
50 def __init__(self, name, parent):
50 def __init__(self, name, parent):
51 super(MagicHelper, self).__init__(name, parent)
51 super(MagicHelper, self).__init__(name, parent)
52
52
53 self.data = None
53 self.data = None
54
54
55 class MinListWidget(QtGui.QListWidget):
55 class MinListWidget(QtGui.QListWidget):
56 """Temp class to overide the default QListWidget size hint
56 """Temp class to overide the default QListWidget size hint
57 in order to make MagicHelper narrow
57 in order to make MagicHelper narrow
58 """
58 """
59 def sizeHint(self):
59 def sizeHint(self):
60 s = QtCore.QSize()
60 s = QtCore.QSize()
61 s.setHeight(super(MinListWidget,self).sizeHint().height())
61 s.setHeight(super(MinListWidget,self).sizeHint().height())
62 s.setWidth(self.sizeHintForColumn(0))
62 s.setWidth(self.sizeHintForColumn(0))
63 return s
63 return s
64
64
65 # construct content
65 # construct content
66 self.frame = QtGui.QFrame()
66 self.frame = QtGui.QFrame()
67 self.search_label = QtGui.QLabel("Search:")
67 self.search_label = QtGui.QLabel("Search:")
68 self.search_line = QtGui.QLineEdit()
68 self.search_line = QtGui.QLineEdit()
69 self.search_class = QtGui.QComboBox()
69 self.search_class = QtGui.QComboBox()
70 self.search_list = MinListWidget()
70 self.search_list = MinListWidget()
71 self.paste_button = QtGui.QPushButton("Paste")
71 self.paste_button = QtGui.QPushButton("Paste")
72 self.run_button = QtGui.QPushButton("Run")
72 self.run_button = QtGui.QPushButton("Run")
73
73
74 # layout all the widgets
74 # layout all the widgets
75 main_layout = QtGui.QVBoxLayout()
75 main_layout = QtGui.QVBoxLayout()
76 search_layout = QtGui.QHBoxLayout()
76 search_layout = QtGui.QHBoxLayout()
77 search_layout.addWidget(self.search_label)
77 search_layout.addWidget(self.search_label)
78 search_layout.addWidget(self.search_line, 10)
78 search_layout.addWidget(self.search_line, 10)
79 main_layout.addLayout(search_layout)
79 main_layout.addLayout(search_layout)
80 main_layout.addWidget(self.search_class)
80 main_layout.addWidget(self.search_class)
81 main_layout.addWidget(self.search_list, 10)
81 main_layout.addWidget(self.search_list, 10)
82 action_layout = QtGui.QHBoxLayout()
82 action_layout = QtGui.QHBoxLayout()
83 action_layout.addWidget(self.paste_button)
83 action_layout.addWidget(self.paste_button)
84 action_layout.addWidget(self.run_button)
84 action_layout.addWidget(self.run_button)
85 main_layout.addLayout(action_layout)
85 main_layout.addLayout(action_layout)
86
86
87 self.frame.setLayout(main_layout)
87 self.frame.setLayout(main_layout)
88 self.setWidget(self.frame)
88 self.setWidget(self.frame)
89
89
90 # connect all the relevant signals to handlers
90 # connect all the relevant signals to handlers
91 self.visibilityChanged[bool].connect( self._update_magic_helper )
91 self.visibilityChanged[bool].connect( self._update_magic_helper )
92 self.search_class.activated[int].connect(
92 self.search_class.activated[int].connect(
93 self.class_selected
93 self.class_selected
94 )
94 )
95 self.search_line.textChanged[str].connect(
95 self.search_line.textChanged[str].connect(
96 self.search_changed
96 self.search_changed
97 )
97 )
98 self.search_list.itemDoubleClicked[QtGui.QListWidgetItem].connect(
98 self.search_list.itemDoubleClicked.connect(
99 self.paste_requested
99 self.paste_requested
100 )
100 )
101 self.paste_button.clicked[bool].connect(
101 self.paste_button.clicked[bool].connect(
102 self.paste_requested
102 self.paste_requested
103 )
103 )
104 self.run_button.clicked[bool].connect(
104 self.run_button.clicked[bool].connect(
105 self.run_requested
105 self.run_requested
106 )
106 )
107
107
108 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
109 # implementation
109 # implementation
110 #---------------------------------------------------------------------------
110 #---------------------------------------------------------------------------
111
111
112 def _update_magic_helper(self, visible):
112 def _update_magic_helper(self, visible):
113 """Start update sequence.
113 """Start update sequence.
114 This method is called when MagicHelper becomes visible. It clears
114 This method is called when MagicHelper becomes visible. It clears
115 the content and emits readyForUpdate signal. The owner of the
115 the content and emits readyForUpdate signal. The owner of the
116 instance is expected to invoke populate_magic_helper() when magic
116 instance is expected to invoke populate_magic_helper() when magic
117 info is available.
117 info is available.
118 """
118 """
119 if not visible or self.data is not None:
119 if not visible or self.data is not None:
120 return
120 return
121 self.data = {}
121 self.data = {}
122 self.search_class.clear()
122 self.search_class.clear()
123 self.search_class.addItem("Populating...")
123 self.search_class.addItem("Populating...")
124 self.search_list.clear()
124 self.search_list.clear()
125 self.readyForUpdate.emit()
125 self.readyForUpdate.emit()
126
126
127 def populate_magic_helper(self, data):
127 def populate_magic_helper(self, data):
128 """Expects data returned by lsmagics query from kernel.
128 """Expects data returned by lsmagics query from kernel.
129 Populates the search_class and search_list with relevant items.
129 Populates the search_class and search_list with relevant items.
130 """
130 """
131 self.search_class.clear()
131 self.search_class.clear()
132 self.search_list.clear()
132 self.search_list.clear()
133
133
134 self.data = json.loads(
134 self.data = json.loads(
135 data['data'].get('application/json', {})
135 data['data'].get('application/json', {})
136 )
136 )
137
137
138 self.search_class.addItem('All Magics', 'any')
138 self.search_class.addItem('All Magics', 'any')
139 classes = set()
139 classes = set()
140
140
141 for mtype in sorted(self.data):
141 for mtype in sorted(self.data):
142 subdict = self.data[mtype]
142 subdict = self.data[mtype]
143 for name in sorted(subdict):
143 for name in sorted(subdict):
144 classes.add(subdict[name])
144 classes.add(subdict[name])
145
145
146 for cls in sorted(classes):
146 for cls in sorted(classes):
147 label = re.sub("([a-zA-Z]+)([A-Z][a-z])","\g<1> \g<2>", cls)
147 label = re.sub("([a-zA-Z]+)([A-Z][a-z])","\g<1> \g<2>", cls)
148 self.search_class.addItem(label, cls)
148 self.search_class.addItem(label, cls)
149
149
150 self.filter_magic_helper('.', 'any')
150 self.filter_magic_helper('.', 'any')
151
151
152 def class_selected(self, index):
152 def class_selected(self, index):
153 """Handle search_class selection changes
153 """Handle search_class selection changes
154 """
154 """
155 item = self.search_class.itemData(index)
155 item = self.search_class.itemData(index)
156 regex = self.search_line.text()
156 regex = self.search_line.text()
157 self.filter_magic_helper(regex = regex, cls = item)
157 self.filter_magic_helper(regex = regex, cls = item)
158
158
159 def search_changed(self, search_string):
159 def search_changed(self, search_string):
160 """Handle search_line text changes.
160 """Handle search_line text changes.
161 The text is interpreted as a regular expression
161 The text is interpreted as a regular expression
162 """
162 """
163 item = self.search_class.itemData(
163 item = self.search_class.itemData(
164 self.search_class.currentIndex()
164 self.search_class.currentIndex()
165 )
165 )
166 self.filter_magic_helper(regex = search_string, cls = item)
166 self.filter_magic_helper(regex = search_string, cls = item)
167
167
168 def _get_current_search_item(self, item = None):
168 def _get_current_search_item(self, item = None):
169 """Retrieve magic command currently selected in the search_list
169 """Retrieve magic command currently selected in the search_list
170 """
170 """
171 text = None
171 text = None
172 if not isinstance(item, QtGui.QListWidgetItem):
172 if not isinstance(item, QtGui.QListWidgetItem):
173 item = self.search_list.currentItem()
173 item = self.search_list.currentItem()
174 text = item.text()
174 text = item.text()
175 return text
175 return text
176
176
177 def paste_requested(self, item = None):
177 def paste_requested(self, item = None):
178 """Emit pasteRequested signal with currently selected item text
178 """Emit pasteRequested signal with currently selected item text
179 """
179 """
180 text = self._get_current_search_item(item)
180 text = self._get_current_search_item(item)
181 if text is not None:
181 if text is not None:
182 self.pasteRequested.emit(text)
182 self.pasteRequested.emit(text)
183
183
184 def run_requested(self, item = None):
184 def run_requested(self, item = None):
185 """Emit runRequested signal with currently selected item text
185 """Emit runRequested signal with currently selected item text
186 """
186 """
187 text = self._get_current_search_item(item)
187 text = self._get_current_search_item(item)
188 if text is not None:
188 if text is not None:
189 self.runRequested.emit(text)
189 self.runRequested.emit(text)
190
190
191 def filter_magic_helper(self, regex, cls):
191 def filter_magic_helper(self, regex, cls):
192 """Update search_list with magic commands whose text match
192 """Update search_list with magic commands whose text match
193 regex and class match cls.
193 regex and class match cls.
194 If cls equals 'any' - any class matches.
194 If cls equals 'any' - any class matches.
195 """
195 """
196 if regex == "" or regex is None:
196 if regex == "" or regex is None:
197 regex = '.'
197 regex = '.'
198 if cls is None:
198 if cls is None:
199 cls = 'any'
199 cls = 'any'
200
200
201 self.search_list.clear()
201 self.search_list.clear()
202 for mtype in sorted(self.data):
202 for mtype in sorted(self.data):
203 subdict = self.data[mtype]
203 subdict = self.data[mtype]
204 prefix = magic_escapes[mtype]
204 prefix = magic_escapes[mtype]
205
205
206 for name in sorted(subdict):
206 for name in sorted(subdict):
207 mclass = subdict[name]
207 mclass = subdict[name]
208 pmagic = prefix + name
208 pmagic = prefix + name
209
209
210 if (re.match(regex, name) or re.match(regex, pmagic)) and \
210 if (re.match(regex, name) or re.match(regex, pmagic)) and \
211 (cls == 'any' or cls == mclass):
211 (cls == 'any' or cls == mclass):
212 self.search_list.addItem(pmagic)
212 self.search_list.addItem(pmagic)
213
213
General Comments 0
You need to be logged in to leave comments. Login now