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