##// END OF EJS Templates
The ConsoleWidget now only pages text if it exceeds the height of the visible window.
The ConsoleWidget now only pages text if it exceeds the height of the visible window.

File last commit:

r2841:58941baa
r2843:f97e4c24
Show More
ipython_widget.py
299 lines | 11.5 KiB | text/x-python | PythonLexer
# Standard library imports
from subprocess import Popen
# System library imports
from PyQt4 import QtCore, QtGui
# Local imports
from IPython.core.inputsplitter import IPythonInputSplitter
from IPython.core.usage import default_banner
from frontend_widget import FrontendWidget
class IPythonPromptBlock(object):
""" An internal storage object for IPythonWidget.
"""
def __init__(self, block, length, number):
self.block = block
self.length = length
self.number = number
class IPythonWidget(FrontendWidget):
""" A FrontendWidget for an IPython kernel.
"""
# Signal emitted when an editor is needed for a file and the editor has been
# specified as 'custom'. See 'set_editor' for more information.
custom_edit_requested = QtCore.pyqtSignal(object, object)
# The default stylesheet: black text on a white background.
default_stylesheet = """
.error { color: red; }
.in-prompt { color: navy; }
.in-prompt-number { font-weight: bold; }
.out-prompt { color: darkred; }
.out-prompt-number { font-weight: bold; }
"""
# A dark stylesheet: white text on a black background.
dark_stylesheet = """
QPlainTextEdit, QTextEdit { background-color: black; color: white }
QFrame { border: 1px solid grey; }
.error { color: red; }
.in-prompt { color: lime; }
.in-prompt-number { color: lime; font-weight: bold; }
.out-prompt { color: red; }
.out-prompt-number { color: red; font-weight: bold; }
"""
# Default prompts.
in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
# FrontendWidget protected class variables.
#_input_splitter_class = IPythonInputSplitter
# IPythonWidget protected class variables.
_payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
_payload_source_page = 'IPython.zmq.page.page'
#---------------------------------------------------------------------------
# 'object' interface
#---------------------------------------------------------------------------
def __init__(self, *args, **kw):
super(IPythonWidget, self).__init__(*args, **kw)
# IPythonWidget protected variables.
self._previous_prompt_obj = None
# Set a default editor and stylesheet.
self.set_editor('default')
self.reset_styling()
#---------------------------------------------------------------------------
# 'BaseFrontendMixin' abstract interface
#---------------------------------------------------------------------------
def _handle_pyout(self, msg):
""" Reimplemented for IPython-style "display hook".
"""
if not self._hidden and self._is_from_this_session(msg):
content = msg['content']
prompt_number = content['prompt_number']
self._append_plain_text(content['output_sep'])
self._append_html(self._make_out_prompt(prompt_number))
self._append_plain_text(content['data'] + '\n' +
content['output_sep2'])
#---------------------------------------------------------------------------
# 'FrontendWidget' interface
#---------------------------------------------------------------------------
def execute_file(self, path, hidden=False):
""" Reimplemented to use the 'run' magic.
"""
self.execute('%%run %s' % path, hidden=hidden)
#---------------------------------------------------------------------------
# 'FrontendWidget' protected interface
#---------------------------------------------------------------------------
def _get_banner(self):
""" Reimplemented to return IPython's default banner.
"""
return default_banner + '\n'
def _process_execute_error(self, msg):
""" Reimplemented for IPython-style traceback formatting.
"""
content = msg['content']
traceback = '\n'.join(content['traceback']) + '\n'
if False:
# FIXME: For now, tracebacks come as plain text, so we can't use
# the html renderer yet. Once we refactor ultratb to produce
# properly styled tracebacks, this branch should be the default
traceback = traceback.replace(' ', '&nbsp;')
traceback = traceback.replace('\n', '<br/>')
ename = content['ename']
ename_styled = '<span class="error">%s</span>' % ename
traceback = traceback.replace(ename, ename_styled)
self._append_html(traceback)
else:
# This is the fallback for now, using plain text with ansi escapes
self._append_plain_text(traceback)
def _process_execute_payload(self, item):
""" Reimplemented to handle %edit and paging payloads.
"""
if item['source'] == self._payload_source_edit:
self.edit(item['filename'], item['line_number'])
return True
elif item['source'] == self._payload_source_page:
self._page(item['data'])
return True
else:
return False
def _show_interpreter_prompt(self, number=None, input_sep='\n'):
""" Reimplemented for IPython-style prompts.
"""
# TODO: If a number was not specified, make a prompt number request.
if number is None:
number = 0
# Show a new prompt and save information about it so that it can be
# updated later if the prompt number turns out to be wrong.
self._append_plain_text(input_sep)
self._show_prompt(self._make_in_prompt(number), html=True)
block = self._control.document().lastBlock()
length = len(self._prompt)
self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
# Update continuation prompt to reflect (possibly) new prompt length.
self._set_continuation_prompt(
self._make_continuation_prompt(self._prompt), html=True)
def _show_interpreter_prompt_for_reply(self, msg):
""" Reimplemented for IPython-style prompts.
"""
# Update the old prompt number if necessary.
content = msg['content']
previous_prompt_number = content['prompt_number']
if self._previous_prompt_obj and \
self._previous_prompt_obj.number != previous_prompt_number:
block = self._previous_prompt_obj.block
if block.isValid():
# Remove the old prompt and insert a new prompt.
cursor = QtGui.QTextCursor(block)
cursor.movePosition(QtGui.QTextCursor.Right,
QtGui.QTextCursor.KeepAnchor,
self._previous_prompt_obj.length)
prompt = self._make_in_prompt(previous_prompt_number)
self._prompt = self._insert_html_fetching_plain_text(
cursor, prompt)
# When the HTML is inserted, Qt blows away the syntax
# highlighting for the line, so we need to rehighlight it.
self._highlighter.rehighlightBlock(cursor.block())
self._previous_prompt_obj = None
# Show a new prompt with the kernel's estimated prompt number.
next_prompt = content['next_prompt']
self._show_interpreter_prompt(next_prompt['prompt_number'],
next_prompt['input_sep'])
#---------------------------------------------------------------------------
# 'IPythonWidget' interface
#---------------------------------------------------------------------------
def edit(self, filename, line=None):
""" Opens a Python script for editing.
Parameters:
-----------
filename : str
A path to a local system file.
line : int, optional
A line of interest in the file.
Raises:
-------
OSError
If the editor command cannot be executed.
"""
if self._editor == 'default':
url = QtCore.QUrl.fromLocalFile(filename)
if not QtGui.QDesktopServices.openUrl(url):
message = 'Failed to open %s with the default application'
raise OSError(message % repr(filename))
elif self._editor is None:
self.custom_edit_requested.emit(filename, line)
else:
Popen(self._editor + [filename])
def reset_styling(self):
""" Restores the default IPythonWidget styling.
"""
self.set_styling(self.default_stylesheet, syntax_style='default')
#self.set_styling(self.dark_stylesheet, syntax_style='monokai')
def set_editor(self, editor):
""" Sets the editor to use with the %edit magic.
Parameters:
-----------
editor : str or sequence of str
A command suitable for use with Popen. This command will be executed
with a single argument--a filename--when editing is requested.
This parameter also takes two special values:
'default' : Files will be edited with the system default
application for Python files.
'custom' : Emit a 'custom_edit_requested(str, int)' signal
instead of opening an editor.
"""
if editor == 'default':
self._editor = 'default'
elif editor == 'custom':
self._editor = None
elif isinstance(editor, basestring):
self._editor = [ editor ]
else:
self._editor = list(editor)
def set_styling(self, stylesheet, syntax_style=None):
""" Sets the IPythonWidget styling.
Parameters:
-----------
stylesheet : str
A CSS stylesheet. The stylesheet can contain classes for:
1. Qt: QPlainTextEdit, QFrame, QWidget, etc
2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
3. IPython: .error, .in-prompt, .out-prompt, etc.
syntax_style : str or None [default None]
If specified, use the Pygments style with given name. Otherwise,
the stylesheet is queried for Pygments style information.
"""
self.setStyleSheet(stylesheet)
self._control.document().setDefaultStyleSheet(stylesheet)
if self._page_control:
self._page_control.document().setDefaultStyleSheet(stylesheet)
if syntax_style is None:
self._highlighter.set_style_sheet(stylesheet)
else:
self._highlighter.set_style(syntax_style)
#---------------------------------------------------------------------------
# 'IPythonWidget' protected interface
#---------------------------------------------------------------------------
def _make_in_prompt(self, number):
""" Given a prompt number, returns an HTML In prompt.
"""
body = self.in_prompt % number
return '<span class="in-prompt">%s</span>' % body
def _make_continuation_prompt(self, prompt):
""" Given a plain text version of an In prompt, returns an HTML
continuation prompt.
"""
end_chars = '...: '
space_count = len(prompt.lstrip('\n')) - len(end_chars)
body = '&nbsp;' * space_count + end_chars
return '<span class="in-prompt">%s</span>' % body
def _make_out_prompt(self, number):
""" Given a prompt number, returns an HTML Out prompt.
"""
body = self.out_prompt % number
return '<span class="out-prompt">%s</span>' % body