##// END OF EJS Templates
BUG: Improve hack. Issue #755. execute_file fails if filename has spaces
jdmarch -
Show More
@@ -1,513 +1,524 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 from textwrap import dedent
16 16
17 17 # System library imports
18 18 from IPython.external.qt import QtCore, QtGui
19 19
20 20 # Local imports
21 21 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 22 transform_ipy_prompt
23 23 from IPython.utils.traitlets import Bool, Unicode
24 24 from frontend_widget import FrontendWidget
25 25 import styles
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Constants
29 29 #-----------------------------------------------------------------------------
30 30
31 31 # Default strings to build and display input and output prompts (and separators
32 32 # in between)
33 33 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
34 34 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 35 default_input_sep = '\n'
36 36 default_output_sep = ''
37 37 default_output_sep2 = ''
38 38
39 39 # Base path for most payload sources.
40 40 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
41 41
42 42 if sys.platform.startswith('win'):
43 43 default_editor = 'notepad'
44 44 else:
45 45 default_editor = ''
46 46
47 47 #-----------------------------------------------------------------------------
48 48 # IPythonWidget class
49 49 #-----------------------------------------------------------------------------
50 50
51 51 class IPythonWidget(FrontendWidget):
52 52 """ A FrontendWidget for an IPython kernel.
53 53 """
54 54
55 55 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 56 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
57 57 # settings.
58 58 custom_edit = Bool(False)
59 59 custom_edit_requested = QtCore.Signal(object, object)
60 60
61 61 editor = Unicode(default_editor, config=True,
62 62 help="""
63 63 A command for invoking a system text editor. If the string contains a
64 64 {filename} format specifier, it will be used. Otherwise, the filename
65 65 will be appended to the end the command.
66 66 """)
67 67
68 68 editor_line = Unicode(config=True,
69 69 help="""
70 70 The editor command to use when a specific line number is requested. The
71 71 string should contain two format specifiers: {line} and {filename}. If
72 72 this parameter is not specified, the line number option to the %edit
73 73 magic will be ignored.
74 74 """)
75 75
76 76 style_sheet = Unicode(config=True,
77 77 help="""
78 78 A CSS stylesheet. The stylesheet can contain classes for:
79 79 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
80 80 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
81 81 3. IPython: .error, .in-prompt, .out-prompt, etc
82 82 """)
83 83
84 84 syntax_style = Unicode(config=True,
85 85 help="""
86 86 If not empty, use this Pygments style for syntax highlighting.
87 87 Otherwise, the style sheet is queried for Pygments style
88 88 information.
89 89 """)
90 90
91 91 # Prompts.
92 92 in_prompt = Unicode(default_in_prompt, config=True)
93 93 out_prompt = Unicode(default_out_prompt, config=True)
94 94 input_sep = Unicode(default_input_sep, config=True)
95 95 output_sep = Unicode(default_output_sep, config=True)
96 96 output_sep2 = Unicode(default_output_sep2, config=True)
97 97
98 98 # FrontendWidget protected class variables.
99 99 _input_splitter_class = IPythonInputSplitter
100 100
101 101 # IPythonWidget protected class variables.
102 102 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
103 103 _payload_source_edit = zmq_shell_source + '.edit_magic'
104 104 _payload_source_exit = zmq_shell_source + '.ask_exit'
105 105 _payload_source_next_input = zmq_shell_source + '.set_next_input'
106 106 _payload_source_page = 'IPython.zmq.page.page'
107 107
108 108 #---------------------------------------------------------------------------
109 109 # 'object' interface
110 110 #---------------------------------------------------------------------------
111 111
112 112 def __init__(self, *args, **kw):
113 113 super(IPythonWidget, self).__init__(*args, **kw)
114 114
115 115 # IPythonWidget protected variables.
116 116 self._payload_handlers = {
117 117 self._payload_source_edit : self._handle_payload_edit,
118 118 self._payload_source_exit : self._handle_payload_exit,
119 119 self._payload_source_page : self._handle_payload_page,
120 120 self._payload_source_next_input : self._handle_payload_next_input }
121 121 self._previous_prompt_obj = None
122 122 self._keep_kernel_on_exit = None
123 123
124 124 # Initialize widget styling.
125 125 if self.style_sheet:
126 126 self._style_sheet_changed()
127 127 self._syntax_style_changed()
128 128 else:
129 129 self.set_default_style()
130 130
131 131 #---------------------------------------------------------------------------
132 132 # 'BaseFrontendMixin' abstract interface
133 133 #---------------------------------------------------------------------------
134 134
135 135 def _handle_complete_reply(self, rep):
136 136 """ Reimplemented to support IPython's improved completion machinery.
137 137 """
138 138 cursor = self._get_cursor()
139 139 info = self._request_info.get('complete')
140 140 if info and info.id == rep['parent_header']['msg_id'] and \
141 141 info.pos == cursor.position():
142 142 matches = rep['content']['matches']
143 143 text = rep['content']['matched_text']
144 144 offset = len(text)
145 145
146 146 # Clean up matches with period and path separators if the matched
147 147 # text has not been transformed. This is done by truncating all
148 148 # but the last component and then suitably decreasing the offset
149 149 # between the current cursor position and the start of completion.
150 150 if len(matches) > 1 and matches[0][:offset] == text:
151 151 parts = re.split(r'[./\\]', text)
152 152 sep_count = len(parts) - 1
153 153 if sep_count:
154 154 chop_length = sum(map(len, parts[:sep_count])) + sep_count
155 155 matches = [ match[chop_length:] for match in matches ]
156 156 offset -= chop_length
157 157
158 158 # Move the cursor to the start of the match and complete.
159 159 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
160 160 self._complete_with_items(cursor, matches)
161 161
162 162 def _handle_execute_reply(self, msg):
163 163 """ Reimplemented to support prompt requests.
164 164 """
165 165 info = self._request_info.get('execute')
166 166 if info and info.id == msg['parent_header']['msg_id']:
167 167 if info.kind == 'prompt':
168 168 number = msg['content']['execution_count'] + 1
169 169 self._show_interpreter_prompt(number)
170 170 else:
171 171 super(IPythonWidget, self)._handle_execute_reply(msg)
172 172
173 173 def _handle_history_reply(self, msg):
174 174 """ Implemented to handle history tail replies, which are only supported
175 175 by the IPython kernel.
176 176 """
177 177 history_items = msg['content']['history']
178 178 items = [ line.rstrip() for _, _, line in history_items ]
179 179 self._set_history(items)
180 180
181 181 def _handle_pyout(self, msg):
182 182 """ Reimplemented for IPython-style "display hook".
183 183 """
184 184 if not self._hidden and self._is_from_this_session(msg):
185 185 content = msg['content']
186 186 prompt_number = content['execution_count']
187 187 data = content['data']
188 188 if data.has_key('text/html'):
189 189 self._append_plain_text(self.output_sep, True)
190 190 self._append_html(self._make_out_prompt(prompt_number), True)
191 191 html = data['text/html']
192 192 self._append_plain_text('\n', True)
193 193 self._append_html(html + self.output_sep2, True)
194 194 elif data.has_key('text/plain'):
195 195 self._append_plain_text(self.output_sep, True)
196 196 self._append_html(self._make_out_prompt(prompt_number), True)
197 197 text = data['text/plain']
198 198 # If the repr is multiline, make sure we start on a new line,
199 199 # so that its lines are aligned.
200 200 if "\n" in text and not self.output_sep.endswith("\n"):
201 201 self._append_plain_text('\n', True)
202 202 self._append_plain_text(text + self.output_sep2, True)
203 203
204 204 def _handle_display_data(self, msg):
205 205 """ The base handler for the ``display_data`` message.
206 206 """
207 207 # For now, we don't display data from other frontends, but we
208 208 # eventually will as this allows all frontends to monitor the display
209 209 # data. But we need to figure out how to handle this in the GUI.
210 210 if not self._hidden and self._is_from_this_session(msg):
211 211 source = msg['content']['source']
212 212 data = msg['content']['data']
213 213 metadata = msg['content']['metadata']
214 214 # In the regular IPythonWidget, we simply print the plain text
215 215 # representation.
216 216 if data.has_key('text/html'):
217 217 html = data['text/html']
218 218 self._append_html(html, True)
219 219 elif data.has_key('text/plain'):
220 220 text = data['text/plain']
221 221 self._append_plain_text(text, True)
222 222 # This newline seems to be needed for text and html output.
223 223 self._append_plain_text(u'\n', True)
224 224
225 225 def _started_channels(self):
226 226 """ Reimplemented to make a history request.
227 227 """
228 228 super(IPythonWidget, self)._started_channels()
229 229 self.kernel_manager.shell_channel.history(hist_access_type='tail',
230 230 n=1000)
231 231 #---------------------------------------------------------------------------
232 232 # 'ConsoleWidget' public interface
233 233 #---------------------------------------------------------------------------
234 234
235 235 def copy(self):
236 236 """ Copy the currently selected text to the clipboard, removing prompts
237 237 if possible.
238 238 """
239 239 text = self._control.textCursor().selection().toPlainText()
240 240 if text:
241 241 lines = map(transform_ipy_prompt, text.splitlines())
242 242 text = '\n'.join(lines)
243 243 QtGui.QApplication.clipboard().setText(text)
244 244
245 245 #---------------------------------------------------------------------------
246 246 # 'FrontendWidget' public interface
247 247 #---------------------------------------------------------------------------
248 248
249 249 def execute_file(self, path, hidden=False):
250 250 """ Reimplemented to use the 'run' magic.
251 251 """
252 252 # Use forward slashes on Windows to avoid escaping each separator.
253 253 if sys.platform == 'win32':
254 254 path = os.path.normpath(path).replace('\\', '/')
255 255
256 # Perhaps we should not be using %run directly, but while we
257 # are, it is necessary to quote filenames containing spaces.
258 if ' ' in path:
259 if '"' not in path:
260 path = '"%s"' % path
261 elif "'" not in path:
262 path = "'%s'" % path
263 else:
264 raise ValueError("Can't run filename containing both single "
265 "and double quotes: %s" % path)
266
256 267 self.execute('%%run %s' % path, hidden=hidden)
257 268
258 269 #---------------------------------------------------------------------------
259 270 # 'FrontendWidget' protected interface
260 271 #---------------------------------------------------------------------------
261 272
262 273 def _complete(self):
263 274 """ Reimplemented to support IPython's improved completion machinery.
264 275 """
265 276 # We let the kernel split the input line, so we *always* send an empty
266 277 # text field. Readline-based frontends do get a real text field which
267 278 # they can use.
268 279 text = ''
269 280
270 281 # Send the completion request to the kernel
271 282 msg_id = self.kernel_manager.shell_channel.complete(
272 283 text, # text
273 284 self._get_input_buffer_cursor_line(), # line
274 285 self._get_input_buffer_cursor_column(), # cursor_pos
275 286 self.input_buffer) # block
276 287 pos = self._get_cursor().position()
277 288 info = self._CompletionRequest(msg_id, pos)
278 289 self._request_info['complete'] = info
279 290
280 291 def _process_execute_error(self, msg):
281 292 """ Reimplemented for IPython-style traceback formatting.
282 293 """
283 294 content = msg['content']
284 295 traceback = '\n'.join(content['traceback']) + '\n'
285 296 if False:
286 297 # FIXME: For now, tracebacks come as plain text, so we can't use
287 298 # the html renderer yet. Once we refactor ultratb to produce
288 299 # properly styled tracebacks, this branch should be the default
289 300 traceback = traceback.replace(' ', '&nbsp;')
290 301 traceback = traceback.replace('\n', '<br/>')
291 302
292 303 ename = content['ename']
293 304 ename_styled = '<span class="error">%s</span>' % ename
294 305 traceback = traceback.replace(ename, ename_styled)
295 306
296 307 self._append_html(traceback)
297 308 else:
298 309 # This is the fallback for now, using plain text with ansi escapes
299 310 self._append_plain_text(traceback)
300 311
301 312 def _process_execute_payload(self, item):
302 313 """ Reimplemented to dispatch payloads to handler methods.
303 314 """
304 315 handler = self._payload_handlers.get(item['source'])
305 316 if handler is None:
306 317 # We have no handler for this type of payload, simply ignore it
307 318 return False
308 319 else:
309 320 handler(item)
310 321 return True
311 322
312 323 def _show_interpreter_prompt(self, number=None):
313 324 """ Reimplemented for IPython-style prompts.
314 325 """
315 326 # If a number was not specified, make a prompt number request.
316 327 if number is None:
317 328 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
318 329 info = self._ExecutionRequest(msg_id, 'prompt')
319 330 self._request_info['execute'] = info
320 331 return
321 332
322 333 # Show a new prompt and save information about it so that it can be
323 334 # updated later if the prompt number turns out to be wrong.
324 335 self._prompt_sep = self.input_sep
325 336 self._show_prompt(self._make_in_prompt(number), html=True)
326 337 block = self._control.document().lastBlock()
327 338 length = len(self._prompt)
328 339 self._previous_prompt_obj = self._PromptBlock(block, length, number)
329 340
330 341 # Update continuation prompt to reflect (possibly) new prompt length.
331 342 self._set_continuation_prompt(
332 343 self._make_continuation_prompt(self._prompt), html=True)
333 344
334 345 def _show_interpreter_prompt_for_reply(self, msg):
335 346 """ Reimplemented for IPython-style prompts.
336 347 """
337 348 # Update the old prompt number if necessary.
338 349 content = msg['content']
339 350 previous_prompt_number = content['execution_count']
340 351 if self._previous_prompt_obj and \
341 352 self._previous_prompt_obj.number != previous_prompt_number:
342 353 block = self._previous_prompt_obj.block
343 354
344 355 # Make sure the prompt block has not been erased.
345 356 if block.isValid() and block.text():
346 357
347 358 # Remove the old prompt and insert a new prompt.
348 359 cursor = QtGui.QTextCursor(block)
349 360 cursor.movePosition(QtGui.QTextCursor.Right,
350 361 QtGui.QTextCursor.KeepAnchor,
351 362 self._previous_prompt_obj.length)
352 363 prompt = self._make_in_prompt(previous_prompt_number)
353 364 self._prompt = self._insert_html_fetching_plain_text(
354 365 cursor, prompt)
355 366
356 367 # When the HTML is inserted, Qt blows away the syntax
357 368 # highlighting for the line, so we need to rehighlight it.
358 369 self._highlighter.rehighlightBlock(cursor.block())
359 370
360 371 self._previous_prompt_obj = None
361 372
362 373 # Show a new prompt with the kernel's estimated prompt number.
363 374 self._show_interpreter_prompt(previous_prompt_number + 1)
364 375
365 376 #---------------------------------------------------------------------------
366 377 # 'IPythonWidget' interface
367 378 #---------------------------------------------------------------------------
368 379
369 380 def set_default_style(self, colors='lightbg'):
370 381 """ Sets the widget style to the class defaults.
371 382
372 383 Parameters:
373 384 -----------
374 385 colors : str, optional (default lightbg)
375 386 Whether to use the default IPython light background or dark
376 387 background or B&W style.
377 388 """
378 389 colors = colors.lower()
379 390 if colors=='lightbg':
380 391 self.style_sheet = styles.default_light_style_sheet
381 392 self.syntax_style = styles.default_light_syntax_style
382 393 elif colors=='linux':
383 394 self.style_sheet = styles.default_dark_style_sheet
384 395 self.syntax_style = styles.default_dark_syntax_style
385 396 elif colors=='nocolor':
386 397 self.style_sheet = styles.default_bw_style_sheet
387 398 self.syntax_style = styles.default_bw_syntax_style
388 399 else:
389 400 raise KeyError("No such color scheme: %s"%colors)
390 401
391 402 #---------------------------------------------------------------------------
392 403 # 'IPythonWidget' protected interface
393 404 #---------------------------------------------------------------------------
394 405
395 406 def _edit(self, filename, line=None):
396 407 """ Opens a Python script for editing.
397 408
398 409 Parameters:
399 410 -----------
400 411 filename : str
401 412 A path to a local system file.
402 413
403 414 line : int, optional
404 415 A line of interest in the file.
405 416 """
406 417 if self.custom_edit:
407 418 self.custom_edit_requested.emit(filename, line)
408 419 elif not self.editor:
409 420 self._append_plain_text('No default editor available.\n'
410 421 'Specify a GUI text editor in the `IPythonWidget.editor` '
411 422 'configurable to enable the %edit magic')
412 423 else:
413 424 try:
414 425 filename = '"%s"' % filename
415 426 if line and self.editor_line:
416 427 command = self.editor_line.format(filename=filename,
417 428 line=line)
418 429 else:
419 430 try:
420 431 command = self.editor.format()
421 432 except KeyError:
422 433 command = self.editor.format(filename=filename)
423 434 else:
424 435 command += ' ' + filename
425 436 except KeyError:
426 437 self._append_plain_text('Invalid editor command.\n')
427 438 else:
428 439 try:
429 440 Popen(command, shell=True)
430 441 except OSError:
431 442 msg = 'Opening editor with command "%s" failed.\n'
432 443 self._append_plain_text(msg % command)
433 444
434 445 def _make_in_prompt(self, number):
435 446 """ Given a prompt number, returns an HTML In prompt.
436 447 """
437 448 try:
438 449 body = self.in_prompt % number
439 450 except TypeError:
440 451 # allow in_prompt to leave out number, e.g. '>>> '
441 452 body = self.in_prompt
442 453 return '<span class="in-prompt">%s</span>' % body
443 454
444 455 def _make_continuation_prompt(self, prompt):
445 456 """ Given a plain text version of an In prompt, returns an HTML
446 457 continuation prompt.
447 458 """
448 459 end_chars = '...: '
449 460 space_count = len(prompt.lstrip('\n')) - len(end_chars)
450 461 body = '&nbsp;' * space_count + end_chars
451 462 return '<span class="in-prompt">%s</span>' % body
452 463
453 464 def _make_out_prompt(self, number):
454 465 """ Given a prompt number, returns an HTML Out prompt.
455 466 """
456 467 body = self.out_prompt % number
457 468 return '<span class="out-prompt">%s</span>' % body
458 469
459 470 #------ Payload handlers --------------------------------------------------
460 471
461 472 # Payload handlers with a generic interface: each takes the opaque payload
462 473 # dict, unpacks it and calls the underlying functions with the necessary
463 474 # arguments.
464 475
465 476 def _handle_payload_edit(self, item):
466 477 self._edit(item['filename'], item['line_number'])
467 478
468 479 def _handle_payload_exit(self, item):
469 480 self._keep_kernel_on_exit = item['keepkernel']
470 481 self.exit_requested.emit()
471 482
472 483 def _handle_payload_next_input(self, item):
473 484 self.input_buffer = dedent(item['text'].rstrip())
474 485
475 486 def _handle_payload_page(self, item):
476 487 # Since the plain text widget supports only a very small subset of HTML
477 488 # and we have no control over the HTML source, we only page HTML
478 489 # payloads in the rich text widget.
479 490 if item['html'] and self.kind == 'rich':
480 491 self._page(item['html'], html=True)
481 492 else:
482 493 self._page(item['text'], html=False)
483 494
484 495 #------ Trait change handlers --------------------------------------------
485 496
486 497 def _style_sheet_changed(self):
487 498 """ Set the style sheets of the underlying widgets.
488 499 """
489 500 self.setStyleSheet(self.style_sheet)
490 501 self._control.document().setDefaultStyleSheet(self.style_sheet)
491 502 if self._page_control:
492 503 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
493 504
494 505 bg_color = self._control.palette().window().color()
495 506 self._ansi_processor.set_background_color(bg_color)
496 507
497 508
498 509 def _syntax_style_changed(self):
499 510 """ Set the style for the syntax highlighter.
500 511 """
501 512 if self._highlighter is None:
502 513 # ignore premature calls
503 514 return
504 515 if self.syntax_style:
505 516 self._highlighter.set_style(self.syntax_style)
506 517 else:
507 518 self._highlighter.set_style_sheet(self.style_sheet)
508 519
509 520 #------ Trait default initializers -----------------------------------------
510 521
511 522 def _banner_default(self):
512 523 from IPython.core.usage import default_gui_banner
513 524 return default_gui_banner
General Comments 0
You need to be logged in to leave comments. Login now