##// END OF EJS Templates
Minor cleanup.
epatters -
Show More
@@ -1,359 +1,367 b''
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8 import zmq
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 13 from call_tip_widget import CallTipWidget
14 14 from completion_lexer import CompletionLexer
15 15 from console_widget import HistoryConsoleWidget
16 16 from pygments_highlighter import PygmentsHighlighter
17 17
18 18
19 19 class FrontendHighlighter(PygmentsHighlighter):
20 20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 21 prompts.
22 22 """
23 23
24 24 def __init__(self, frontend):
25 25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 26 self._current_offset = 0
27 27 self._frontend = frontend
28 28 self.highlighting_on = False
29 29
30 30 def highlightBlock(self, qstring):
31 31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 32 """
33 33 if not self.highlighting_on:
34 34 return
35 35
36 36 # The input to this function is unicode string that may contain
37 37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 38 # the string as plain text so we can compare it.
39 39 current_block = self.currentBlock()
40 40 string = self._frontend._get_block_plain_text(current_block)
41 41
42 42 # Decide whether to check for the regular or continuation prompt.
43 43 if current_block.contains(self._frontend._prompt_pos):
44 44 prompt = self._frontend._prompt
45 45 else:
46 46 prompt = self._frontend._continuation_prompt
47 47
48 48 # Don't highlight the part of the string that contains the prompt.
49 49 if string.startswith(prompt):
50 50 self._current_offset = len(prompt)
51 51 qstring.remove(0, len(prompt))
52 52 else:
53 53 self._current_offset = 0
54 54
55 55 PygmentsHighlighter.highlightBlock(self, qstring)
56 56
57 def rehighlightBlock(self, block):
58 """ Reimplemented to temporarily enable highlighting if disabled.
59 """
60 old = self.highlighting_on
61 self.highlighting_on = True
62 super(FrontendHighlighter, self).rehighlightBlock(block)
63 self.highlighting_on = old
64
57 65 def setFormat(self, start, count, format):
58 66 """ Reimplemented to highlight selectively.
59 67 """
60 68 start += self._current_offset
61 69 PygmentsHighlighter.setFormat(self, start, count, format)
62 70
63 71
64 72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
65 73 """ A Qt frontend for a generic Python kernel.
66 74 """
67 75
68 76 # Emitted when an 'execute_reply' has been received from the kernel and
69 77 # processed by the FrontendWidget.
70 78 executed = QtCore.pyqtSignal(object)
71 79
72 80 # Protected class attributes.
73 81 _highlighter_class = FrontendHighlighter
74 82 _input_splitter_class = InputSplitter
75 83
76 84 #---------------------------------------------------------------------------
77 85 # 'object' interface
78 86 #---------------------------------------------------------------------------
79 87
80 88 def __init__(self, *args, **kw):
81 89 super(FrontendWidget, self).__init__(*args, **kw)
82 90
83 91 # FrontendWidget protected variables.
84 92 self._call_tip_widget = CallTipWidget(self._control)
85 93 self._completion_lexer = CompletionLexer(PythonLexer())
86 94 self._hidden = False
87 95 self._highlighter = self._highlighter_class(self)
88 96 self._input_splitter = self._input_splitter_class(input_mode='replace')
89 97 self._kernel_manager = None
90 98
91 99 # Configure the ConsoleWidget.
92 100 self.tab_width = 4
93 101 self._set_continuation_prompt('... ')
94 102
95 103 # Connect signal handlers.
96 104 document = self._control.document()
97 105 document.contentsChange.connect(self._document_contents_change)
98 106
99 107 #---------------------------------------------------------------------------
100 108 # 'ConsoleWidget' abstract interface
101 109 #---------------------------------------------------------------------------
102 110
103 111 def _is_complete(self, source, interactive):
104 112 """ Returns whether 'source' can be completely processed and a new
105 113 prompt created. When triggered by an Enter/Return key press,
106 114 'interactive' is True; otherwise, it is False.
107 115 """
108 116 complete = self._input_splitter.push(source.expandtabs(4))
109 117 if interactive:
110 118 complete = not self._input_splitter.push_accepts_more()
111 119 return complete
112 120
113 121 def _execute(self, source, hidden):
114 122 """ Execute 'source'. If 'hidden', do not show any output.
115 123 """
116 124 self.kernel_manager.xreq_channel.execute(source)
117 125 self._hidden = hidden
118 126
119 127 def _execute_interrupt(self):
120 128 """ Attempts to stop execution. Returns whether this method has an
121 129 implementation.
122 130 """
123 131 self._interrupt_kernel()
124 132 return True
125 133
126 134 def _prompt_started_hook(self):
127 135 """ Called immediately after a new prompt is displayed.
128 136 """
129 137 if not self._reading:
130 138 self._highlighter.highlighting_on = True
131 139
132 140 def _prompt_finished_hook(self):
133 141 """ Called immediately after a prompt is finished, i.e. when some input
134 142 will be processed and a new prompt displayed.
135 143 """
136 144 if not self._reading:
137 145 self._highlighter.highlighting_on = False
138 146
139 147 def _tab_pressed(self):
140 148 """ Called when the tab key is pressed. Returns whether to continue
141 149 processing the event.
142 150 """
143 151 self._keep_cursor_in_buffer()
144 152 cursor = self._get_cursor()
145 153 return not self._complete()
146 154
147 155 #---------------------------------------------------------------------------
148 156 # 'ConsoleWidget' protected interface
149 157 #---------------------------------------------------------------------------
150 158
151 159 def _show_continuation_prompt(self):
152 160 """ Reimplemented for auto-indentation.
153 161 """
154 162 super(FrontendWidget, self)._show_continuation_prompt()
155 163 spaces = self._input_splitter.indent_spaces
156 164 self._append_plain_text('\t' * (spaces / self.tab_width))
157 165 self._append_plain_text(' ' * (spaces % self.tab_width))
158 166
159 167 #---------------------------------------------------------------------------
160 168 # 'BaseFrontendMixin' abstract interface
161 169 #---------------------------------------------------------------------------
162 170
163 171 def _handle_complete_reply(self, rep):
164 172 """ Handle replies for tab completion.
165 173 """
166 174 cursor = self._get_cursor()
167 175 if rep['parent_header']['msg_id'] == self._complete_id and \
168 176 cursor.position() == self._complete_pos:
169 177 text = '.'.join(self._get_context())
170 178 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
171 179 self._complete_with_items(cursor, rep['content']['matches'])
172 180
173 181 def _handle_execute_reply(self, msg):
174 182 """ Handles replies for code execution.
175 183 """
176 184 if not self._hidden:
177 185 # Make sure that all output from the SUB channel has been processed
178 186 # before writing a new prompt.
179 187 self.kernel_manager.sub_channel.flush()
180 188
181 189 content = msg['content']
182 190 status = content['status']
183 191 if status == 'ok':
184 192 self._process_execute_ok(msg)
185 193 elif status == 'error':
186 194 self._process_execute_error(msg)
187 195 elif status == 'abort':
188 196 self._process_execute_abort(msg)
189 197
190 198 self._show_interpreter_prompt_for_reply(msg)
191 199 self.executed.emit(msg)
192 200
193 201 def _handle_input_request(self, msg):
194 202 """ Handle requests for raw_input.
195 203 """
196 204 if self._hidden:
197 205 raise RuntimeError('Request for raw input during hidden execution.')
198 206
199 207 # Make sure that all output from the SUB channel has been processed
200 208 # before entering readline mode.
201 209 self.kernel_manager.sub_channel.flush()
202 210
203 211 def callback(line):
204 212 self.kernel_manager.rep_channel.input(line)
205 213 self._readline(msg['content']['prompt'], callback=callback)
206 214
207 215 def _handle_object_info_reply(self, rep):
208 216 """ Handle replies for call tips.
209 217 """
210 218 cursor = self._get_cursor()
211 219 if rep['parent_header']['msg_id'] == self._call_tip_id and \
212 220 cursor.position() == self._call_tip_pos:
213 221 doc = rep['content']['docstring']
214 222 if doc:
215 223 self._call_tip_widget.show_docstring(doc)
216 224
217 225 def _handle_pyout(self, msg):
218 226 """ Handle display hook output.
219 227 """
220 228 if not self._hidden and self._is_from_this_session(msg):
221 229 self._append_plain_text(msg['content']['data'] + '\n')
222 230
223 231 def _handle_stream(self, msg):
224 232 """ Handle stdout, stderr, and stdin.
225 233 """
226 234 if not self._hidden and self._is_from_this_session(msg):
227 235 self._append_plain_text(msg['content']['data'])
228 236 self._control.moveCursor(QtGui.QTextCursor.End)
229 237
230 238 def _started_channels(self):
231 239 """ Called when the KernelManager channels have started listening or
232 240 when the frontend is assigned an already listening KernelManager.
233 241 """
234 242 self._reset()
235 243 self._append_plain_text(self._get_banner())
236 244 self._show_interpreter_prompt()
237 245
238 246 def _stopped_channels(self):
239 247 """ Called when the KernelManager channels have stopped listening or
240 248 when a listening KernelManager is removed from the frontend.
241 249 """
242 250 # FIXME: Print a message here?
243 251 pass
244 252
245 253 #---------------------------------------------------------------------------
246 254 # 'FrontendWidget' interface
247 255 #---------------------------------------------------------------------------
248 256
249 257 def execute_file(self, path, hidden=False):
250 258 """ Attempts to execute file with 'path'. If 'hidden', no output is
251 259 shown.
252 260 """
253 261 self.execute('execfile("%s")' % path, hidden=hidden)
254 262
255 263 #---------------------------------------------------------------------------
256 264 # 'FrontendWidget' protected interface
257 265 #---------------------------------------------------------------------------
258 266
259 267 def _call_tip(self):
260 268 """ Shows a call tip, if appropriate, at the current cursor location.
261 269 """
262 270 # Decide if it makes sense to show a call tip
263 271 cursor = self._get_cursor()
264 272 cursor.movePosition(QtGui.QTextCursor.Left)
265 273 document = self._control.document()
266 274 if document.characterAt(cursor.position()).toAscii() != '(':
267 275 return False
268 276 context = self._get_context(cursor)
269 277 if not context:
270 278 return False
271 279
272 280 # Send the metadata request to the kernel
273 281 name = '.'.join(context)
274 282 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
275 283 self._call_tip_pos = self._get_cursor().position()
276 284 return True
277 285
278 286 def _complete(self):
279 287 """ Performs completion at the current cursor location.
280 288 """
281 289 # Decide if it makes sense to do completion
282 290 context = self._get_context()
283 291 if not context:
284 292 return False
285 293
286 294 # Send the completion request to the kernel
287 295 text = '.'.join(context)
288 296 self._complete_id = self.kernel_manager.xreq_channel.complete(
289 297 text, self._get_input_buffer_cursor_line(), self.input_buffer)
290 298 self._complete_pos = self._get_cursor().position()
291 299 return True
292 300
293 301 def _get_banner(self):
294 302 """ Gets a banner to display at the beginning of a session.
295 303 """
296 304 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
297 305 '"license" for more information.'
298 306 return banner % (sys.version, sys.platform)
299 307
300 308 def _get_context(self, cursor=None):
301 309 """ Gets the context at the current cursor location.
302 310 """
303 311 if cursor is None:
304 312 cursor = self._get_cursor()
305 313 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
306 314 QtGui.QTextCursor.KeepAnchor)
307 315 text = str(cursor.selection().toPlainText())
308 316 return self._completion_lexer.get_context(text)
309 317
310 318 def _interrupt_kernel(self):
311 319 """ Attempts to the interrupt the kernel.
312 320 """
313 321 if self.kernel_manager.has_kernel:
314 322 self.kernel_manager.signal_kernel(signal.SIGINT)
315 323 else:
316 324 self._append_plain_text('Kernel process is either remote or '
317 325 'unspecified. Cannot interrupt.\n')
318 326
319 327 def _process_execute_abort(self, msg):
320 328 """ Process a reply for an aborted execution request.
321 329 """
322 330 self._append_plain_text("ERROR: execution aborted\n")
323 331
324 332 def _process_execute_error(self, msg):
325 333 """ Process a reply for an execution request that resulted in an error.
326 334 """
327 335 content = msg['content']
328 336 traceback = ''.join(content['traceback'])
329 337 self._append_plain_text(traceback)
330 338
331 339 def _process_execute_ok(self, msg):
332 340 """ Process a reply for a successful execution equest.
333 341 """
334 342 # The basic FrontendWidget doesn't handle payloads, as they are a
335 343 # mechanism for going beyond the standard Python interpreter model.
336 344 pass
337 345
338 346 def _show_interpreter_prompt(self):
339 347 """ Shows a prompt for the interpreter.
340 348 """
341 349 self._show_prompt('>>> ')
342 350
343 351 def _show_interpreter_prompt_for_reply(self, msg):
344 352 """ Shows a prompt for the interpreter given an 'execute_reply' message.
345 353 """
346 354 self._show_interpreter_prompt()
347 355
348 356 #------ Signal handlers ----------------------------------------------------
349 357
350 358 def _document_contents_change(self, position, removed, added):
351 359 """ Called whenever the document's content changes. Display a call tip
352 360 if appropriate.
353 361 """
354 362 # Calculate where the cursor should be *after* the change:
355 363 position += added
356 364
357 365 document = self._control.document()
358 366 if position == self._get_cursor().position():
359 367 self._call_tip()
@@ -1,277 +1,274 b''
1 1 # Standard library imports
2 2 from subprocess import Popen
3 3
4 4 # System library imports
5 5 from PyQt4 import QtCore, QtGui
6 6
7 7 # Local imports
8 8 from IPython.core.inputsplitter import IPythonInputSplitter
9 9 from IPython.core.usage import default_banner
10 10 from frontend_widget import FrontendWidget
11 11
12 12
13 13 class IPythonPromptBlock(object):
14 14 """ An internal storage object for IPythonWidget.
15 15 """
16 16 def __init__(self, block, length, number):
17 17 self.block = block
18 18 self.length = length
19 19 self.number = number
20 20
21 21
22 22 class IPythonWidget(FrontendWidget):
23 23 """ A FrontendWidget for an IPython kernel.
24 24 """
25 25
26 26 # Signal emitted when an editor is needed for a file and the editor has been
27 27 # specified as 'custom'.
28 28 custom_edit_requested = QtCore.pyqtSignal(object)
29 29
30 30 # The default stylesheet: black text on a white background.
31 31 default_stylesheet = """
32 32 .error { color: red; }
33 33 .in-prompt { color: navy; }
34 34 .in-prompt-number { font-weight: bold; }
35 35 .out-prompt { color: darkred; }
36 36 .out-prompt-number { font-weight: bold; }
37 37 """
38 38
39 39 # A dark stylesheet: white text on a black background.
40 40 dark_stylesheet = """
41 41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
42 42 QFrame { border: 1px solid grey; }
43 43 .error { color: red; }
44 44 .in-prompt { color: lime; }
45 45 .in-prompt-number { color: lime; font-weight: bold; }
46 46 .out-prompt { color: red; }
47 47 .out-prompt-number { color: red; font-weight: bold; }
48 48 """
49 49
50 50 # Default prompts.
51 51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
52 52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
53 53
54 54 # FrontendWidget protected class attributes.
55 55 #_input_splitter_class = IPythonInputSplitter
56 56
57 57 #---------------------------------------------------------------------------
58 58 # 'object' interface
59 59 #---------------------------------------------------------------------------
60 60
61 61 def __init__(self, *args, **kw):
62 62 super(IPythonWidget, self).__init__(*args, **kw)
63 63
64 64 # IPythonWidget protected variables.
65 65 self._previous_prompt_obj = None
66 66
67 67 # Set a default editor and stylesheet.
68 68 self.set_editor('default')
69 69 self.reset_styling()
70 70
71 71 #---------------------------------------------------------------------------
72 72 # 'BaseFrontendMixin' abstract interface
73 73 #---------------------------------------------------------------------------
74 74
75 75 def _handle_pyout(self, msg):
76 76 """ Reimplemented for IPython-style "display hook".
77 77 """
78 78 if not self._hidden and self._is_from_this_session(msg):
79 79 content = msg['content']
80 80 prompt_number = content['prompt_number']
81 81 self._append_plain_text(content['output_sep'])
82 82 self._append_html(self._make_out_prompt(prompt_number))
83 83 self._append_plain_text(content['data'] + '\n' +
84 84 content['output_sep2'])
85 85
86 86 #---------------------------------------------------------------------------
87 87 # 'FrontendWidget' interface
88 88 #---------------------------------------------------------------------------
89 89
90 90 def execute_file(self, path, hidden=False):
91 91 """ Reimplemented to use the 'run' magic.
92 92 """
93 93 self.execute('run %s' % path, hidden=hidden)
94 94
95 95 #---------------------------------------------------------------------------
96 96 # 'FrontendWidget' protected interface
97 97 #---------------------------------------------------------------------------
98 98
99 99 def _get_banner(self):
100 100 """ Reimplemented to return IPython's default banner.
101 101 """
102 102 return default_banner + '\n'
103 103
104 104 def _process_execute_error(self, msg):
105 105 """ Reimplemented for IPython-style traceback formatting.
106 106 """
107 107 content = msg['content']
108 108 traceback_lines = content['traceback'][:]
109 109 traceback = ''.join(traceback_lines)
110 110 traceback = traceback.replace(' ', '&nbsp;')
111 111 traceback = traceback.replace('\n', '<br/>')
112 112
113 113 ename = content['ename']
114 114 ename_styled = '<span class="error">%s</span>' % ename
115 115 traceback = traceback.replace(ename, ename_styled)
116 116
117 117 self._append_html(traceback)
118 118
119 119 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
120 120 """ Reimplemented for IPython-style prompts.
121 121 """
122 122 # TODO: If a number was not specified, make a prompt number request.
123 123 if number is None:
124 124 number = 0
125 125
126 126 # Show a new prompt and save information about it so that it can be
127 127 # updated later if the prompt number turns out to be wrong.
128 128 self._append_plain_text(input_sep)
129 129 self._show_prompt(self._make_in_prompt(number), html=True)
130 130 block = self._control.document().lastBlock()
131 131 length = len(self._prompt)
132 132 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
133 133
134 134 # Update continuation prompt to reflect (possibly) new prompt length.
135 135 self._set_continuation_prompt(
136 136 self._make_continuation_prompt(self._prompt), html=True)
137 137
138 138 def _show_interpreter_prompt_for_reply(self, msg):
139 139 """ Reimplemented for IPython-style prompts.
140 140 """
141 141 # Update the old prompt number if necessary.
142 142 content = msg['content']
143 143 previous_prompt_number = content['prompt_number']
144 144 if self._previous_prompt_obj and \
145 145 self._previous_prompt_obj.number != previous_prompt_number:
146 146 block = self._previous_prompt_obj.block
147 147 if block.isValid():
148 148
149 149 # Remove the old prompt and insert a new prompt.
150 150 cursor = QtGui.QTextCursor(block)
151 151 cursor.movePosition(QtGui.QTextCursor.Right,
152 152 QtGui.QTextCursor.KeepAnchor,
153 153 self._previous_prompt_obj.length)
154 154 prompt = self._make_in_prompt(previous_prompt_number)
155 155 self._prompt = self._insert_html_fetching_plain_text(
156 156 cursor, prompt)
157 157
158 # XXX: When the HTML is inserted, Qt blows away the syntax
159 # highlighting for the line. I cannot for the life of me
160 # determine how to preserve the existing formatting.
161 self._highlighter.highlighting_on = True
158 # When the HTML is inserted, Qt blows away the syntax
159 # highlighting for the line, so we need to rehighlight it.
162 160 self._highlighter.rehighlightBlock(cursor.block())
163 self._highlighter.highlighting_on = False
164 161
165 162 self._previous_prompt_obj = None
166 163
167 164 # Show a new prompt with the kernel's estimated prompt number.
168 165 next_prompt = content['next_prompt']
169 166 self._show_interpreter_prompt(next_prompt['prompt_number'],
170 167 next_prompt['input_sep'])
171 168
172 169 #---------------------------------------------------------------------------
173 170 # 'IPythonWidget' interface
174 171 #---------------------------------------------------------------------------
175 172
176 173 def edit(self, filename):
177 174 """ Opens a Python script for editing.
178 175
179 176 Parameters:
180 177 -----------
181 178 filename : str
182 179 A path to a local system file.
183 180
184 181 Raises:
185 182 -------
186 183 OSError
187 184 If the editor command cannot be executed.
188 185 """
189 186 if self._editor == 'default':
190 187 url = QtCore.QUrl.fromLocalFile(filename)
191 188 if not QtGui.QDesktopServices.openUrl(url):
192 189 message = 'Failed to open %s with the default application'
193 190 raise OSError(message % repr(filename))
194 191 elif self._editor is None:
195 192 self.custom_edit_requested.emit(filename)
196 193 else:
197 194 Popen(self._editor + [filename])
198 195
199 196 def reset_styling(self):
200 197 """ Restores the default IPythonWidget styling.
201 198 """
202 199 self.set_styling(self.default_stylesheet, syntax_style='default')
203 200 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
204 201
205 202 def set_editor(self, editor):
206 203 """ Sets the editor to use with the %edit magic.
207 204
208 205 Parameters:
209 206 -----------
210 207 editor : str or sequence of str
211 208 A command suitable for use with Popen. This command will be executed
212 209 with a single argument--a filename--when editing is requested.
213 210
214 211 This parameter also takes two special values:
215 212 'default' : Files will be edited with the system default
216 213 application for Python files.
217 214 'custom' : Emit a 'custom_edit_requested(str)' signal instead
218 215 of opening an editor.
219 216 """
220 217 if editor == 'default':
221 218 self._editor = 'default'
222 219 elif editor == 'custom':
223 220 self._editor = None
224 221 elif isinstance(editor, basestring):
225 222 self._editor = [ editor ]
226 223 else:
227 224 self._editor = list(editor)
228 225
229 226 def set_styling(self, stylesheet, syntax_style=None):
230 227 """ Sets the IPythonWidget styling.
231 228
232 229 Parameters:
233 230 -----------
234 231 stylesheet : str
235 232 A CSS stylesheet. The stylesheet can contain classes for:
236 233 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
237 234 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
238 235 3. IPython: .error, .in-prompt, .out-prompt, etc.
239 236
240 237 syntax_style : str or None [default None]
241 238 If specified, use the Pygments style with given name. Otherwise,
242 239 the stylesheet is queried for Pygments style information.
243 240 """
244 241 self.setStyleSheet(stylesheet)
245 242 self._control.document().setDefaultStyleSheet(stylesheet)
246 243 if self._page_control:
247 244 self._page_control.document().setDefaultStyleSheet(stylesheet)
248 245
249 246 if syntax_style is None:
250 247 self._highlighter.set_style_sheet(stylesheet)
251 248 else:
252 249 self._highlighter.set_style(syntax_style)
253 250
254 251 #---------------------------------------------------------------------------
255 252 # 'IPythonWidget' protected interface
256 253 #---------------------------------------------------------------------------
257 254
258 255 def _make_in_prompt(self, number):
259 256 """ Given a prompt number, returns an HTML In prompt.
260 257 """
261 258 body = self.in_prompt % number
262 259 return '<span class="in-prompt">%s</span>' % body
263 260
264 261 def _make_continuation_prompt(self, prompt):
265 262 """ Given a plain text version of an In prompt, returns an HTML
266 263 continuation prompt.
267 264 """
268 265 end_chars = '...: '
269 266 space_count = len(prompt.lstrip('\n')) - len(end_chars)
270 267 body = '&nbsp;' * space_count + end_chars
271 268 return '<span class="in-prompt">%s</span>' % body
272 269
273 270 def _make_out_prompt(self, number):
274 271 """ Given a prompt number, returns an HTML Out prompt.
275 272 """
276 273 body = self.out_prompt % number
277 274 return '<span class="out-prompt">%s</span>' % body
@@ -1,379 +1,378 b''
1 1 #!/usr/bin/env python
2 2 """A simple interactive kernel that talks to a frontend over 0MQ.
3 3
4 4 Things to do:
5 5
6 6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
7 7 call set_parent on all the PUB objects with the message about to be executed.
8 8 * Implement random port and security key logic.
9 9 * Implement control messages.
10 10 * Implement event loop and poll version.
11 11 """
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 # Standard library imports.
18 18 import __builtin__
19 19 import sys
20 20 import time
21 21 import traceback
22 22
23 23 # System library imports.
24 24 import zmq
25 25
26 26 # Local imports.
27 27 from IPython.config.configurable import Configurable
28 28 from IPython.utils.traitlets import Instance
29 29 from completer import KernelCompleter
30 30 from entry_point import base_launch_kernel, make_argument_parser, make_kernel, \
31 31 start_kernel
32 32 from iostream import OutStream
33 33 from session import Session, Message
34 34 from zmqshell import ZMQInteractiveShell
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Main kernel class
38 38 #-----------------------------------------------------------------------------
39 39
40 40 class Kernel(Configurable):
41 41
42 42 #---------------------------------------------------------------------------
43 43 # Kernel interface
44 44 #---------------------------------------------------------------------------
45 45
46 46 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
47 47 session = Instance(Session)
48 48 reply_socket = Instance('zmq.Socket')
49 49 pub_socket = Instance('zmq.Socket')
50 50 req_socket = Instance('zmq.Socket')
51 51
52 52 # Maps user-friendly backend names to matplotlib backend identifiers.
53 53 _pylab_map = { 'tk': 'TkAgg',
54 54 'gtk': 'GTKAgg',
55 55 'wx': 'WXAgg',
56 56 'qt': 'Qt4Agg', # qt3 not supported
57 57 'qt4': 'Qt4Agg',
58 58 'payload-svg' : \
59 59 'module://IPython.zmq.pylab.backend_payload_svg' }
60 60
61 61 def __init__(self, **kwargs):
62 62 super(Kernel, self).__init__(**kwargs)
63 63
64 64 # Initialize the InteractiveShell subclass
65 65 self.shell = ZMQInteractiveShell.instance()
66 66 self.shell.displayhook.session = self.session
67 67 self.shell.displayhook.pub_socket = self.pub_socket
68 68
69 69 # Build dict of handlers for message types
70 70 msg_types = [ 'execute_request', 'complete_request',
71 71 'object_info_request', 'prompt_request',
72 72 'history_request' ]
73 73 self.handlers = {}
74 74 for msg_type in msg_types:
75 75 self.handlers[msg_type] = getattr(self, msg_type)
76 76
77 77 def activate_pylab(self, backend=None, import_all=True):
78 78 """ Activates pylab in this kernel's namespace.
79 79
80 80 Parameters:
81 81 -----------
82 82 backend : str, optional
83 83 A valid backend name.
84 84
85 85 import_all : bool, optional
86 86 If true, an 'import *' is done from numpy and pylab.
87 87 """
88 88 # FIXME: This is adapted from IPython.lib.pylabtools.pylab_activate.
89 # Common funtionality should be refactored.
89 # Common functionality should be refactored.
90 90
91 91 # We must set the desired backend before importing pylab.
92 92 import matplotlib
93 93 if backend:
94 94 backend_id = self._pylab_map[backend]
95 95 if backend_id.startswith('module://'):
96 96 # Work around bug in matplotlib: matplotlib.use converts the
97 97 # backend_id to lowercase even if a module name is specified!
98 98 matplotlib.rcParams['backend'] = backend_id
99 99 else:
100 100 matplotlib.use(backend_id)
101 101
102 102 # Import numpy as np/pyplot as plt are conventions we're trying to
103 103 # somewhat standardize on. Making them available to users by default
104 104 # will greatly help this.
105 105 exec ("import numpy\n"
106 106 "import matplotlib\n"
107 107 "from matplotlib import pylab, mlab, pyplot\n"
108 108 "np = numpy\n"
109 109 "plt = pyplot\n"
110 110 ) in self.shell.user_ns
111 111
112 112 if import_all:
113 113 exec("from matplotlib.pylab import *\n"
114 114 "from numpy import *\n") in self.shell.user_ns
115 115
116 116 matplotlib.interactive(True)
117 117
118 118 def start(self):
119 119 """ Start the kernel main loop.
120 120 """
121
122 121 while True:
123 122 ident = self.reply_socket.recv()
124 123 assert self.reply_socket.rcvmore(), "Missing message part."
125 124 msg = self.reply_socket.recv_json()
126 125 omsg = Message(msg)
127 126 print>>sys.__stdout__
128 127 print>>sys.__stdout__, omsg
129 128 handler = self.handlers.get(omsg.msg_type, None)
130 129 if handler is None:
131 130 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
132 131 else:
133 132 handler(ident, omsg)
134 133
135 134 #---------------------------------------------------------------------------
136 135 # Kernel request handlers
137 136 #---------------------------------------------------------------------------
138 137
139 138 def execute_request(self, ident, parent):
140 139 try:
141 140 code = parent[u'content'][u'code']
142 141 except:
143 142 print>>sys.__stderr__, "Got bad msg: "
144 143 print>>sys.__stderr__, Message(parent)
145 144 return
146 145 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
147 146 self.pub_socket.send_json(pyin_msg)
148 147
149 148 try:
150 149 # Replace raw_input. Note that is not sufficient to replace
151 150 # raw_input in the user namespace.
152 151 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
153 152 __builtin__.raw_input = raw_input
154 153
155 154 # Set the parent message of the display hook and out streams.
156 155 self.shell.displayhook.set_parent(parent)
157 156 sys.stdout.set_parent(parent)
158 157 sys.stderr.set_parent(parent)
159 158
160 159 self.shell.runlines(code)
161 160 except:
162 161 etype, evalue, tb = sys.exc_info()
163 162 tb = traceback.format_exception(etype, evalue, tb)
164 163 exc_content = {
165 164 u'status' : u'error',
166 165 u'traceback' : tb,
167 166 u'ename' : unicode(etype.__name__),
168 167 u'evalue' : unicode(evalue)
169 168 }
170 169 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
171 170 self.pub_socket.send_json(exc_msg)
172 171 reply_content = exc_content
173 172 else:
174 173 payload = self.shell.payload_manager.read_payload()
175 174 # Be agressive about clearing the payload because we don't want
176 175 # it to sit in memory until the next execute_request comes in.
177 176 self.shell.payload_manager.clear_payload()
178 177 reply_content = { 'status' : 'ok', 'payload' : payload }
179 178
180 179 # Compute the prompt information
181 180 prompt_number = self.shell.displayhook.prompt_count
182 181 reply_content['prompt_number'] = prompt_number
183 182 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
184 183 next_prompt = {'prompt_string' : prompt_string,
185 184 'prompt_number' : prompt_number+1,
186 185 'input_sep' : self.shell.displayhook.input_sep}
187 186 reply_content['next_prompt'] = next_prompt
188 187
189 188 # Flush output before sending the reply.
190 189 sys.stderr.flush()
191 190 sys.stdout.flush()
192 191
193 192 # Send the reply.
194 193 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
195 194 print>>sys.__stdout__, Message(reply_msg)
196 195 self.reply_socket.send(ident, zmq.SNDMORE)
197 196 self.reply_socket.send_json(reply_msg)
198 197 if reply_msg['content']['status'] == u'error':
199 198 self._abort_queue()
200 199
201 200 def complete_request(self, ident, parent):
202 201 matches = {'matches' : self._complete(parent),
203 202 'status' : 'ok'}
204 203 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
205 204 matches, parent, ident)
206 205 print >> sys.__stdout__, completion_msg
207 206
208 207 def object_info_request(self, ident, parent):
209 208 context = parent['content']['oname'].split('.')
210 209 object_info = self._object_info(context)
211 210 msg = self.session.send(self.reply_socket, 'object_info_reply',
212 211 object_info, parent, ident)
213 212 print >> sys.__stdout__, msg
214 213
215 214 def prompt_request(self, ident, parent):
216 215 prompt_number = self.shell.displayhook.prompt_count
217 216 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
218 217 content = {'prompt_string' : prompt_string,
219 218 'prompt_number' : prompt_number+1}
220 219 msg = self.session.send(self.reply_socket, 'prompt_reply',
221 220 content, parent, ident)
222 221 print >> sys.__stdout__, msg
223 222
224 223 def history_request(self, ident, parent):
225 224 output = parent['content'].get('output', True)
226 225 index = parent['content'].get('index')
227 226 raw = parent['content'].get('raw', False)
228 227 hist = self.shell.get_history(index=index, raw=raw, output=output)
229 228 content = {'history' : hist}
230 229 msg = self.session.send(self.reply_socket, 'history_reply',
231 230 content, parent, ident)
232 231 print >> sys.__stdout__, msg
233 232
234 233 #---------------------------------------------------------------------------
235 234 # Protected interface
236 235 #---------------------------------------------------------------------------
237 236
238 237 def _abort_queue(self):
239 238 while True:
240 239 try:
241 240 ident = self.reply_socket.recv(zmq.NOBLOCK)
242 241 except zmq.ZMQError, e:
243 242 if e.errno == zmq.EAGAIN:
244 243 break
245 244 else:
246 245 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
247 246 msg = self.reply_socket.recv_json()
248 247 print>>sys.__stdout__, "Aborting:"
249 248 print>>sys.__stdout__, Message(msg)
250 249 msg_type = msg['msg_type']
251 250 reply_type = msg_type.split('_')[0] + '_reply'
252 251 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
253 252 print>>sys.__stdout__, Message(reply_msg)
254 253 self.reply_socket.send(ident,zmq.SNDMORE)
255 254 self.reply_socket.send_json(reply_msg)
256 255 # We need to wait a bit for requests to come in. This can probably
257 256 # be set shorter for true asynchronous clients.
258 257 time.sleep(0.1)
259 258
260 259 def _raw_input(self, prompt, ident, parent):
261 260 # Flush output before making the request.
262 261 sys.stderr.flush()
263 262 sys.stdout.flush()
264 263
265 264 # Send the input request.
266 265 content = dict(prompt=prompt)
267 266 msg = self.session.msg(u'input_request', content, parent)
268 267 self.req_socket.send_json(msg)
269 268
270 269 # Await a response.
271 270 reply = self.req_socket.recv_json()
272 271 try:
273 272 value = reply['content']['value']
274 273 except:
275 274 print>>sys.__stderr__, "Got bad raw_input reply: "
276 275 print>>sys.__stderr__, Message(parent)
277 276 value = ''
278 277 return value
279 278
280 279 def _complete(self, msg):
281 280 return self.shell.complete(msg.content.line)
282 281
283 282 def _object_info(self, context):
284 283 symbol, leftover = self._symbol_from_context(context)
285 284 if symbol is not None and not leftover:
286 285 doc = getattr(symbol, '__doc__', '')
287 286 else:
288 287 doc = ''
289 288 object_info = dict(docstring = doc)
290 289 return object_info
291 290
292 291 def _symbol_from_context(self, context):
293 292 if not context:
294 293 return None, context
295 294
296 295 base_symbol_string = context[0]
297 296 symbol = self.shell.user_ns.get(base_symbol_string, None)
298 297 if symbol is None:
299 298 symbol = __builtin__.__dict__.get(base_symbol_string, None)
300 299 if symbol is None:
301 300 return None, context
302 301
303 302 context = context[1:]
304 303 for i, name in enumerate(context):
305 304 new_symbol = getattr(symbol, name, None)
306 305 if new_symbol is None:
307 306 return symbol, context[i:]
308 307 else:
309 308 symbol = new_symbol
310 309
311 310 return symbol, []
312 311
313 312 #-----------------------------------------------------------------------------
314 313 # Kernel main and launch functions
315 314 #-----------------------------------------------------------------------------
316 315
317 316 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False,
318 317 pylab=False):
319 318 """ Launches a localhost kernel, binding to the specified ports.
320 319
321 320 Parameters
322 321 ----------
323 322 xrep_port : int, optional
324 323 The port to use for XREP channel.
325 324
326 325 pub_port : int, optional
327 326 The port to use for the SUB channel.
328 327
329 328 req_port : int, optional
330 329 The port to use for the REQ (raw input) channel.
331 330
332 331 independent : bool, optional (default False)
333 332 If set, the kernel process is guaranteed to survive if this process
334 333 dies. If not set, an effort is made to ensure that the kernel is killed
335 334 when this process dies. Note that in this case it is still good practice
336 335 to kill kernels manually before exiting.
337 336
338 337 pylab : bool or string, optional (default False)
339 338 If not False, the kernel will be launched with pylab enabled. If a
340 339 string is passed, matplotlib will use the specified backend. Otherwise,
341 340 matplotlib's default backend will be used.
342 341
343 342 Returns
344 343 -------
345 344 A tuple of form:
346 345 (kernel_process, xrep_port, pub_port, req_port)
347 346 where kernel_process is a Popen object and the ports are integers.
348 347 """
349 348 extra_arguments = []
350 349 if pylab:
351 350 extra_arguments.append('--pylab')
352 351 if isinstance(pylab, basestring):
353 352 extra_arguments.append(pylab)
354 353 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
355 354 xrep_port, pub_port, req_port, independent,
356 355 extra_arguments)
357 356
358 357 def main():
359 358 """ The IPython kernel main entry point.
360 359 """
361 360 parser = make_argument_parser()
362 361 parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
363 362 const='auto', help = \
364 363 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
365 364 given, the GUI backend is matplotlib's, otherwise use one of: \
366 365 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
367 366 namespace = parser.parse_args()
368 367
369 368 kernel = make_kernel(namespace, Kernel, OutStream)
370 369 if namespace.pylab:
371 370 if namespace.pylab == 'auto':
372 371 kernel.activate_pylab()
373 372 else:
374 373 kernel.activate_pylab(namespace.pylab)
375 374
376 375 start_kernel(namespace, kernel)
377 376
378 377 if __name__ == '__main__':
379 378 main()
General Comments 0
You need to be logged in to leave comments. Login now