Uploaded Test files

This commit is contained in:
Batuhan Berk Başoğlu 2020-11-12 11:05:57 -05:00
parent f584ad9d97
commit 2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions

View file

@ -0,0 +1 @@
from ._version import version_info, __version__

View file

@ -0,0 +1,3 @@
if __name__ == '__main__':
from qtconsole.qtconsoleapp import main
main()

View file

@ -0,0 +1,2 @@
version_info = (4, 7, 7)
__version__ = '.'.join(map(str, version_info))

View file

@ -0,0 +1,393 @@
""" Utilities for processing ANSI escape codes and special ASCII characters.
"""
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
# Standard library imports
from collections import namedtuple
import re
# System library imports
from qtpy import QtGui
# Local imports
from ipython_genutils.py3compat import string_types
from qtconsole.styles import dark_style
#-----------------------------------------------------------------------------
# Constants and datatypes
#-----------------------------------------------------------------------------
# An action for erase requests (ED and EL commands).
EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
# An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
# and HVP commands).
# FIXME: Not implemented in AnsiCodeProcessor.
MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
# An action for scroll requests (SU and ST) and form feeds.
ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
# An action for the carriage return character
CarriageReturnAction = namedtuple('CarriageReturnAction', ['action'])
# An action for the \n character
NewLineAction = namedtuple('NewLineAction', ['action'])
# An action for the beep character
BeepAction = namedtuple('BeepAction', ['action'])
# An action for backspace
BackSpaceAction = namedtuple('BackSpaceAction', ['action'])
# Regular expressions.
CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu'
CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS
OSC_SUBPATTERN = '\](.*?)[\x07\x1b]'
ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \
(CSI_SUBPATTERN, OSC_SUBPATTERN))
ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN)
SPECIAL_PATTERN = re.compile('([\f])')
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
class AnsiCodeProcessor(object):
""" Translates special ASCII characters and ANSI escape codes into readable
attributes. It also supports a few non-standard, xterm-specific codes.
"""
# Whether to increase intensity or set boldness for SGR code 1.
# (Different terminals handle this in different ways.)
bold_text_enabled = False
# We provide an empty default color map because subclasses will likely want
# to use a custom color format.
default_color_map = {}
#---------------------------------------------------------------------------
# AnsiCodeProcessor interface
#---------------------------------------------------------------------------
def __init__(self):
self.actions = []
self.color_map = self.default_color_map.copy()
self.reset_sgr()
def reset_sgr(self):
""" Reset graphics attributs to their default values.
"""
self.intensity = 0
self.italic = False
self.bold = False
self.underline = False
self.foreground_color = None
self.background_color = None
def split_string(self, string):
""" Yields substrings for which the same escape code applies.
"""
self.actions = []
start = 0
# strings ending with \r are assumed to be ending in \r\n since
# \n is appended to output strings automatically. Accounting
# for that, here.
last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None
string = string[:-1] if last_char is not None else string
for match in ANSI_OR_SPECIAL_PATTERN.finditer(string):
raw = string[start:match.start()]
substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
if substring or self.actions:
yield substring
self.actions = []
start = match.end()
groups = [g for g in match.groups() if (g is not None)]
g0 = groups[0]
if g0 == '\a':
self.actions.append(BeepAction('beep'))
yield None
self.actions = []
elif g0 == '\r':
self.actions.append(CarriageReturnAction('carriage-return'))
yield None
self.actions = []
elif g0 == '\b':
self.actions.append(BackSpaceAction('backspace'))
yield None
self.actions = []
elif g0 == '\n' or g0 == '\r\n':
self.actions.append(NewLineAction('newline'))
yield g0
self.actions = []
else:
params = [ param for param in groups[1].split(';') if param ]
if g0.startswith('['):
# Case 1: CSI code.
try:
params = list(map(int, params))
except ValueError:
# Silently discard badly formed codes.
pass
else:
self.set_csi_code(groups[2], params)
elif g0.startswith(']'):
# Case 2: OSC code.
self.set_osc_code(params)
raw = string[start:]
substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
if substring or self.actions:
yield substring
if last_char is not None:
self.actions.append(NewLineAction('newline'))
yield last_char
def set_csi_code(self, command, params=[]):
""" Set attributes based on CSI (Control Sequence Introducer) code.
Parameters
----------
command : str
The code identifier, i.e. the final character in the sequence.
params : sequence of integers, optional
The parameter codes for the command.
"""
if command == 'm': # SGR - Select Graphic Rendition
if params:
self.set_sgr_code(params)
else:
self.set_sgr_code([0])
elif (command == 'J' or # ED - Erase Data
command == 'K'): # EL - Erase in Line
code = params[0] if params else 0
if 0 <= code <= 2:
area = 'screen' if command == 'J' else 'line'
if code == 0:
erase_to = 'end'
elif code == 1:
erase_to = 'start'
elif code == 2:
erase_to = 'all'
self.actions.append(EraseAction('erase', area, erase_to))
elif (command == 'S' or # SU - Scroll Up
command == 'T'): # SD - Scroll Down
dir = 'up' if command == 'S' else 'down'
count = params[0] if params else 1
self.actions.append(ScrollAction('scroll', dir, 'line', count))
def set_osc_code(self, params):
""" Set attributes based on OSC (Operating System Command) parameters.
Parameters
----------
params : sequence of str
The parameters for the command.
"""
try:
command = int(params.pop(0))
except (IndexError, ValueError):
return
if command == 4:
# xterm-specific: set color number to color spec.
try:
color = int(params.pop(0))
spec = params.pop(0)
self.color_map[color] = self._parse_xterm_color_spec(spec)
except (IndexError, ValueError):
pass
def set_sgr_code(self, params):
""" Set attributes based on SGR (Select Graphic Rendition) codes.
Parameters
----------
params : sequence of ints
A list of SGR codes for one or more SGR commands. Usually this
sequence will have one element per command, although certain
xterm-specific commands requires multiple elements.
"""
# Always consume the first parameter.
if not params:
return
code = params.pop(0)
if code == 0:
self.reset_sgr()
elif code == 1:
if self.bold_text_enabled:
self.bold = True
else:
self.intensity = 1
elif code == 2:
self.intensity = 0
elif code == 3:
self.italic = True
elif code == 4:
self.underline = True
elif code == 22:
self.intensity = 0
self.bold = False
elif code == 23:
self.italic = False
elif code == 24:
self.underline = False
elif code >= 30 and code <= 37:
self.foreground_color = code - 30
elif code == 38 and params:
_color_type = params.pop(0)
if _color_type == 5 and params:
# xterm-specific: 256 color support.
self.foreground_color = params.pop(0)
elif _color_type == 2:
# 24bit true colour support.
self.foreground_color = params[:3]
params[:3] = []
elif code == 39:
self.foreground_color = None
elif code >= 40 and code <= 47:
self.background_color = code - 40
elif code == 48 and params:
_color_type = params.pop(0)
if _color_type == 5 and params:
# xterm-specific: 256 color support.
self.background_color = params.pop(0)
elif _color_type == 2:
# 24bit true colour support.
self.background_color = params[:3]
params[:3] = []
elif code == 49:
self.background_color = None
# Recurse with unconsumed parameters.
self.set_sgr_code(params)
#---------------------------------------------------------------------------
# Protected interface
#---------------------------------------------------------------------------
def _parse_xterm_color_spec(self, spec):
if spec.startswith('rgb:'):
return tuple(map(lambda x: int(x, 16), spec[4:].split('/')))
elif spec.startswith('rgbi:'):
return tuple(map(lambda x: int(float(x) * 255),
spec[5:].split('/')))
elif spec == '?':
raise ValueError('Unsupported xterm color spec')
return spec
def _replace_special(self, match):
special = match.group(1)
if special == '\f':
self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
return ''
class QtAnsiCodeProcessor(AnsiCodeProcessor):
""" Translates ANSI escape codes into QTextCharFormats.
"""
# A map from ANSI color codes to SVG color names or RGB(A) tuples.
darkbg_color_map = {
0 : 'black', # black
1 : 'darkred', # red
2 : 'darkgreen', # green
3 : 'brown', # yellow
4 : 'darkblue', # blue
5 : 'darkviolet', # magenta
6 : 'steelblue', # cyan
7 : 'grey', # white
8 : 'grey', # black (bright)
9 : 'red', # red (bright)
10 : 'lime', # green (bright)
11 : 'yellow', # yellow (bright)
12 : 'deepskyblue', # blue (bright)
13 : 'magenta', # magenta (bright)
14 : 'cyan', # cyan (bright)
15 : 'white' } # white (bright)
# Set the default color map for super class.
default_color_map = darkbg_color_map.copy()
def get_color(self, color, intensity=0):
""" Returns a QColor for a given color code or rgb list, or None if one
cannot be constructed.
"""
if isinstance(color, int):
# Adjust for intensity, if possible.
if color < 8 and intensity > 0:
color += 8
constructor = self.color_map.get(color, None)
elif isinstance(color, (tuple, list)):
constructor = color
else:
return None
if isinstance(constructor, string_types):
# If this is an X11 color name, we just hope there is a close SVG
# color name. We could use QColor's static method
# 'setAllowX11ColorNames()', but this is global and only available
# on X11. It seems cleaner to aim for uniformity of behavior.
return QtGui.QColor(constructor)
elif isinstance(constructor, (tuple, list)):
return QtGui.QColor(*constructor)
return None
def get_format(self):
""" Returns a QTextCharFormat that encodes the current style attributes.
"""
format = QtGui.QTextCharFormat()
# Set foreground color
qcolor = self.get_color(self.foreground_color, self.intensity)
if qcolor is not None:
format.setForeground(qcolor)
# Set background color
qcolor = self.get_color(self.background_color, self.intensity)
if qcolor is not None:
format.setBackground(qcolor)
# Set font weight/style options
if self.bold:
format.setFontWeight(QtGui.QFont.Bold)
else:
format.setFontWeight(QtGui.QFont.Normal)
format.setFontItalic(self.italic)
format.setFontUnderline(self.underline)
return format
def set_background_color(self, style):
"""
Given a syntax style, attempt to set a color map that will be
aesthetically pleasing.
"""
# Set a new default color map.
self.default_color_map = self.darkbg_color_map.copy()
if not dark_style(style):
# Colors appropriate for a terminal with a light background. For
# now, only use non-bright colors...
for i in range(8):
self.default_color_map[i + 8] = self.default_color_map[i]
# ...and replace white with black.
self.default_color_map[7] = self.default_color_map[15] = 'black'
# Update the current color map with the new defaults.
self.color_map.update(self.default_color_map)

View file

@ -0,0 +1,158 @@
"""Defines a convenient mix-in class for implementing Qt frontends."""
class BaseFrontendMixin(object):
""" A mix-in class for implementing Qt frontends.
To handle messages of a particular type, frontends need only define an
appropriate handler method. For example, to handle 'stream' messaged, define
a '_handle_stream(msg)' method.
"""
#---------------------------------------------------------------------------
# 'BaseFrontendMixin' concrete interface
#---------------------------------------------------------------------------
_kernel_client = None
_kernel_manager = None
@property
def kernel_client(self):
"""Returns the current kernel client."""
return self._kernel_client
@kernel_client.setter
def kernel_client(self, kernel_client):
"""Disconnect from the current kernel client (if any) and set a new
kernel client.
"""
# Disconnect the old kernel client, if necessary.
old_client = self._kernel_client
if old_client is not None:
old_client.started_channels.disconnect(self._started_channels)
old_client.stopped_channels.disconnect(self._stopped_channels)
# Disconnect the old kernel client's channels.
old_client.iopub_channel.message_received.disconnect(self._dispatch)
old_client.shell_channel.message_received.disconnect(self._dispatch)
old_client.stdin_channel.message_received.disconnect(self._dispatch)
old_client.hb_channel.kernel_died.disconnect(
self._handle_kernel_died)
# Handle the case where the old kernel client is still listening.
if old_client.channels_running:
self._stopped_channels()
# Set the new kernel client.
self._kernel_client = kernel_client
if kernel_client is None:
return
# Connect the new kernel client.
kernel_client.started_channels.connect(self._started_channels)
kernel_client.stopped_channels.connect(self._stopped_channels)
# Connect the new kernel client's channels.
kernel_client.iopub_channel.message_received.connect(self._dispatch)
kernel_client.shell_channel.message_received.connect(self._dispatch)
kernel_client.stdin_channel.message_received.connect(self._dispatch)
# hb_channel
kernel_client.hb_channel.kernel_died.connect(self._handle_kernel_died)
# Handle the case where the kernel client started channels before
# we connected.
if kernel_client.channels_running:
self._started_channels()
@property
def kernel_manager(self):
"""The kernel manager, if any"""
return self._kernel_manager
@kernel_manager.setter
def kernel_manager(self, kernel_manager):
old_man = self._kernel_manager
if old_man is not None:
old_man.kernel_restarted.disconnect(self._handle_kernel_restarted)
self._kernel_manager = kernel_manager
if kernel_manager is None:
return
kernel_manager.kernel_restarted.connect(self._handle_kernel_restarted)
#---------------------------------------------------------------------------
# 'BaseFrontendMixin' abstract interface
#---------------------------------------------------------------------------
def _handle_kernel_died(self, since_last_heartbeat):
""" This is called when the ``kernel_died`` signal is emitted.
This method is called when the kernel heartbeat has not been
active for a certain amount of time.
This is a strictly passive notification -
the kernel is likely being restarted by its KernelManager.
Parameters
----------
since_last_heartbeat : float
The time since the heartbeat was last received.
"""
def _handle_kernel_restarted(self):
""" This is called when the ``kernel_restarted`` signal is emitted.
This method is called when the kernel has been restarted by the
autorestart mechanism.
Parameters
----------
since_last_heartbeat : float
The time since the heartbeat was last received.
"""
def _started_kernel(self):
"""Called when the KernelManager starts (or restarts) the kernel subprocess.
Channels may or may not be running at this point.
"""
def _started_channels(self):
""" Called when the KernelManager channels have started listening or
when the frontend is assigned an already listening KernelManager.
"""
def _stopped_channels(self):
""" Called when the KernelManager channels have stopped listening or
when a listening KernelManager is removed from the frontend.
"""
#---------------------------------------------------------------------------
# 'BaseFrontendMixin' protected interface
#---------------------------------------------------------------------------
def _dispatch(self, msg):
""" Calls the frontend handler associated with the message type of the
given message.
"""
msg_type = msg['header']['msg_type']
handler = getattr(self, '_handle_' + msg_type, None)
if handler:
handler(msg)
def from_here(self, msg):
"""Return whether a message is from this session"""
session_id = self._kernel_client.session.session
return msg['parent_header'].get("session", session_id) == session_id
def include_output(self, msg):
"""Return whether we should include a given output message"""
if self._hidden:
return False
from_here = self.from_here(msg)
if msg['msg_type'] == 'execute_input':
# only echo inputs not from here
return self.include_other_output and not from_here
if self.include_other_output:
return True
else:
return from_here

View file

@ -0,0 +1,100 @@
""" Provides bracket matching for Q[Plain]TextEdit widgets.
"""
# System library imports
from qtpy import QtCore, QtGui, QtWidgets
class BracketMatcher(QtCore.QObject):
""" Matches square brackets, braces, and parentheses based on cursor
position.
"""
# Protected class variables.
_opening_map = { '(':')', '{':'}', '[':']' }
_closing_map = { ')':'(', '}':'{', ']':'[' }
#--------------------------------------------------------------------------
# 'QObject' interface
#--------------------------------------------------------------------------
def __init__(self, text_edit):
""" Create a call tip manager that is attached to the specified Qt
text edit widget.
"""
assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
super(BracketMatcher, self).__init__()
# The format to apply to matching brackets.
self.format = QtGui.QTextCharFormat()
self.format.setBackground(QtGui.QColor('silver'))
self._text_edit = text_edit
text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
#--------------------------------------------------------------------------
# Protected interface
#--------------------------------------------------------------------------
def _find_match(self, position):
""" Given a valid position in the text document, try to find the
position of the matching bracket. Returns -1 if unsuccessful.
"""
# Decide what character to search for and what direction to search in.
document = self._text_edit.document()
start_char = document.characterAt(position)
search_char = self._opening_map.get(start_char)
if search_char:
increment = 1
else:
search_char = self._closing_map.get(start_char)
if search_char:
increment = -1
else:
return -1
# Search for the character.
char = start_char
depth = 0
while position >= 0 and position < document.characterCount():
if char == start_char:
depth += 1
elif char == search_char:
depth -= 1
if depth == 0:
break
position += increment
char = document.characterAt(position)
else:
position = -1
return position
def _selection_for_character(self, position):
""" Convenience method for selecting a character.
"""
selection = QtWidgets.QTextEdit.ExtraSelection()
cursor = self._text_edit.textCursor()
cursor.setPosition(position)
cursor.movePosition(QtGui.QTextCursor.NextCharacter,
QtGui.QTextCursor.KeepAnchor)
selection.cursor = cursor
selection.format = self.format
return selection
#------ Signal handlers ----------------------------------------------------
def _cursor_position_changed(self):
""" Updates the document formatting based on the new cursor position.
"""
# Clear out the old formatting.
self._text_edit.setExtraSelections([])
# Attempt to match a bracket for the new cursor position.
cursor = self._text_edit.textCursor()
if not cursor.hasSelection():
position = cursor.position() - 1
match_position = self._find_match(position)
if match_position != -1:
extra_selections = [ self._selection_for_character(pos)
for pos in (position, match_position) ]
self._text_edit.setExtraSelections(extra_selections)

View file

@ -0,0 +1,265 @@
# Standard library imports
import re
from unicodedata import category
# System library imports
from qtpy import QtCore, QtGui, QtWidgets
class CallTipWidget(QtWidgets.QLabel):
""" Shows call tips by parsing the current text of Q[Plain]TextEdit.
"""
#--------------------------------------------------------------------------
# 'QObject' interface
#--------------------------------------------------------------------------
def __init__(self, text_edit):
""" Create a call tip manager that is attached to the specified Qt
text edit widget.
"""
assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip)
self._hide_timer = QtCore.QBasicTimer()
self._text_edit = text_edit
self.setFont(text_edit.document().defaultFont())
self.setForegroundRole(QtGui.QPalette.ToolTipText)
self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
self.setPalette(QtWidgets.QToolTip.palette())
self.setAlignment(QtCore.Qt.AlignLeft)
self.setIndent(1)
self.setFrameStyle(QtWidgets.QFrame.NoFrame)
self.setMargin(1 + self.style().pixelMetric(
QtWidgets.QStyle.PM_ToolTipLabelFrameWidth, None, self))
self.setWindowOpacity(self.style().styleHint(
QtWidgets.QStyle.SH_ToolTipLabel_Opacity, None, self, None) / 255.0)
self.setWordWrap(True)
def eventFilter(self, obj, event):
""" Reimplemented to hide on certain key presses and on text edit focus
changes.
"""
if obj == self._text_edit:
etype = event.type()
if etype == QtCore.QEvent.KeyPress:
key = event.key()
if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.hide()
elif key == QtCore.Qt.Key_Escape:
self.hide()
return True
elif etype == QtCore.QEvent.FocusOut:
self.hide()
elif etype == QtCore.QEvent.Enter:
self._hide_timer.stop()
elif etype == QtCore.QEvent.Leave:
self._leave_event_hide()
return super(CallTipWidget, self).eventFilter(obj, event)
def timerEvent(self, event):
""" Reimplemented to hide the widget when the hide timer fires.
"""
if event.timerId() == self._hide_timer.timerId():
self._hide_timer.stop()
self.hide()
#--------------------------------------------------------------------------
# 'QWidget' interface
#--------------------------------------------------------------------------
def enterEvent(self, event):
""" Reimplemented to cancel the hide timer.
"""
super(CallTipWidget, self).enterEvent(event)
self._hide_timer.stop()
def hideEvent(self, event):
""" Reimplemented to disconnect signal handlers and event filter.
"""
super(CallTipWidget, self).hideEvent(event)
# This fixes issue jupyter/qtconsole#383
try:
self._text_edit.cursorPositionChanged.disconnect(
self._cursor_position_changed)
except TypeError:
pass
self._text_edit.removeEventFilter(self)
def leaveEvent(self, event):
""" Reimplemented to start the hide timer.
"""
super(CallTipWidget, self).leaveEvent(event)
self._leave_event_hide()
def paintEvent(self, event):
""" Reimplemented to paint the background panel.
"""
painter = QtWidgets.QStylePainter(self)
option = QtWidgets.QStyleOptionFrame()
option.initFrom(self)
painter.drawPrimitive(QtWidgets.QStyle.PE_PanelTipLabel, option)
painter.end()
super(CallTipWidget, self).paintEvent(event)
def setFont(self, font):
""" Reimplemented to allow use of this method as a slot.
"""
super(CallTipWidget, self).setFont(font)
def showEvent(self, event):
""" Reimplemented to connect signal handlers and event filter.
"""
super(CallTipWidget, self).showEvent(event)
self._text_edit.cursorPositionChanged.connect(
self._cursor_position_changed)
self._text_edit.installEventFilter(self)
#--------------------------------------------------------------------------
# 'CallTipWidget' interface
#--------------------------------------------------------------------------
def show_inspect_data(self, content, maxlines=20):
"""Show inspection data as a tooltip"""
data = content.get('data', {})
text = data.get('text/plain', '')
match = re.match("(?:[^\n]*\n){%i}" % maxlines, text)
if match:
text = text[:match.end()] + '\n[Documentation continues...]'
return self.show_tip(self._format_tooltip(text))
def show_tip(self, tip):
""" Attempts to show the specified tip at the current cursor location.
"""
# Attempt to find the cursor position at which to show the call tip.
text_edit = self._text_edit
document = text_edit.document()
cursor = text_edit.textCursor()
search_pos = cursor.position() - 1
self._start_position, _ = self._find_parenthesis(search_pos,
forward=False)
if self._start_position == -1:
return False
# Set the text and resize the widget accordingly.
self.setText(tip)
self.resize(self.sizeHint())
# Locate and show the widget. Place the tip below the current line
# unless it would be off the screen. In that case, decide the best
# location based trying to minimize the area that goes off-screen.
padding = 3 # Distance in pixels between cursor bounds and tip box.
cursor_rect = text_edit.cursorRect(cursor)
screen_rect = QtWidgets.QApplication.instance().desktop().screenGeometry(text_edit)
point = text_edit.mapToGlobal(cursor_rect.bottomRight())
point.setY(point.y() + padding)
tip_height = self.size().height()
tip_width = self.size().width()
vertical = 'bottom'
horizontal = 'Right'
if point.y() + tip_height > screen_rect.height() + screen_rect.y():
point_ = text_edit.mapToGlobal(cursor_rect.topRight())
# If tip is still off screen, check if point is in top or bottom
# half of screen.
if point_.y() - tip_height < padding:
# If point is in upper half of screen, show tip below it.
# otherwise above it.
if 2*point.y() < screen_rect.height():
vertical = 'bottom'
else:
vertical = 'top'
else:
vertical = 'top'
if point.x() + tip_width > screen_rect.width() + screen_rect.x():
point_ = text_edit.mapToGlobal(cursor_rect.topRight())
# If tip is still off-screen, check if point is in the right or
# left half of the screen.
if point_.x() - tip_width < padding:
if 2*point.x() < screen_rect.width():
horizontal = 'Right'
else:
horizontal = 'Left'
else:
horizontal = 'Left'
pos = getattr(cursor_rect, '%s%s' %(vertical, horizontal))
point = text_edit.mapToGlobal(pos())
point.setY(point.y() + padding)
if vertical == 'top':
point.setY(point.y() - tip_height)
if horizontal == 'Left':
point.setX(point.x() - tip_width - padding)
self.move(point)
self.show()
return True
#--------------------------------------------------------------------------
# Protected interface
#--------------------------------------------------------------------------
def _find_parenthesis(self, position, forward=True):
""" If 'forward' is True (resp. False), proceed forwards
(resp. backwards) through the line that contains 'position' until an
unmatched closing (resp. opening) parenthesis is found. Returns a
tuple containing the position of this parenthesis (or -1 if it is
not found) and the number commas (at depth 0) found along the way.
"""
commas = depth = 0
document = self._text_edit.document()
char = document.characterAt(position)
# Search until a match is found or a non-printable character is
# encountered.
while category(char) != 'Cc' and position > 0:
if char == ',' and depth == 0:
commas += 1
elif char == ')':
if forward and depth == 0:
break
depth += 1
elif char == '(':
if not forward and depth == 0:
break
depth -= 1
position += 1 if forward else -1
char = document.characterAt(position)
else:
position = -1
return position, commas
def _leave_event_hide(self):
""" Hides the tooltip after some time has passed (assuming the cursor is
not over the tooltip).
"""
if (not self._hide_timer.isActive() and
# If Enter events always came after Leave events, we wouldn't need
# this check. But on Mac OS, it sometimes happens the other way
# around when the tooltip is created.
QtWidgets.QApplication.instance().topLevelAt(QtGui.QCursor.pos()) != self):
self._hide_timer.start(300, self)
def _format_tooltip(self, doc):
doc = re.sub(r'\033\[(\d|;)+?m', '', doc)
return doc
#------ Signal handlers ----------------------------------------------------
def _cursor_position_changed(self):
""" Updates the tip based on user cursor movement.
"""
cursor = self._text_edit.textCursor()
if cursor.position() <= self._start_position:
self.hide()
else:
position, commas = self._find_parenthesis(self._start_position + 1)
if position != -1:
self.hide()

View file

@ -0,0 +1,71 @@
""" Defines a KernelClient that provides signals and slots.
"""
import atexit
import errno
from threading import Thread
import time
import zmq
# import ZMQError in top-level namespace, to avoid ugly attribute-error messages
# during garbage collection of threads at exit:
from zmq import ZMQError
from zmq.eventloop import ioloop, zmqstream
from qtpy import QtCore
# Local imports
from traitlets import Type, Instance
from jupyter_client.channels import HBChannel
from jupyter_client import KernelClient
from jupyter_client.channels import InvalidPortNumber
from jupyter_client.threaded import ThreadedKernelClient, ThreadedZMQSocketChannel
from .kernel_mixins import QtKernelClientMixin
from .util import SuperQObject
class QtHBChannel(SuperQObject, HBChannel):
# A longer timeout than the base class
time_to_dead = 3.0
# Emitted when the kernel has died.
kernel_died = QtCore.Signal(object)
def call_handlers(self, since_last_heartbeat):
""" Reimplemented to emit signals instead of making callbacks.
"""
# Emit the generic signal.
self.kernel_died.emit(since_last_heartbeat)
from jupyter_client import protocol_version_info
major_protocol_version = protocol_version_info[0]
class QtZMQSocketChannel(ThreadedZMQSocketChannel,SuperQObject):
"""A ZMQ socket emitting a Qt signal when a message is received."""
message_received = QtCore.Signal(object)
def process_events(self):
""" Process any pending GUI events.
"""
QtCore.QCoreApplication.instance().processEvents()
def call_handlers(self, msg):
"""This method is called in the ioloop thread when a message arrives.
It is important to remember that this method is called in the thread
so that some logic must be done to ensure that the application level
handlers are called in the application thread.
"""
# Emit the generic signal.
self.message_received.emit(msg)
class QtKernelClient(QtKernelClientMixin, ThreadedKernelClient):
""" A KernelClient that provides signals and slots.
"""
iopub_channel_class = Type(QtZMQSocketChannel)
shell_channel_class = Type(QtZMQSocketChannel)
stdin_channel_class = Type(QtZMQSocketChannel)
hb_channel_class = Type(QtHBChannel)

View file

@ -0,0 +1,274 @@
"""
Based on
https://github.com/jupyter/notebook/blob/master/notebook/static/services/kernels/comm.js
https://github.com/ipython/ipykernel/blob/master/ipykernel/comm/manager.py
https://github.com/ipython/ipykernel/blob/master/ipykernel/comm/comm.py
Which are distributed under the terms of the Modified BSD License.
"""
import logging
from traitlets.config import LoggingConfigurable
from ipython_genutils.importstring import import_item
from ipython_genutils.py3compat import string_types
import uuid
from qtpy import QtCore
from qtconsole.util import MetaQObjectHasTraits, SuperQObject
class CommManager(MetaQObjectHasTraits(
'NewBase', (LoggingConfigurable, SuperQObject), {})):
"""
Manager for Comms in the Frontend
"""
def __init__(self, kernel_client, *args, **kwargs):
super(CommManager, self).__init__(*args, **kwargs)
self.comms = {}
self.targets = {}
if kernel_client:
self.init_kernel_client(kernel_client)
def init_kernel_client(self, kernel_client):
"""
connect the kernel, and register message handlers
"""
self.kernel_client = kernel_client
kernel_client.iopub_channel.message_received.connect(self._dispatch)
@QtCore.Slot(object)
def _dispatch(self, msg):
"""Dispatch messages"""
msg_type = msg['header']['msg_type']
handled_msg_types = ['comm_open', 'comm_msg', 'comm_close']
if msg_type in handled_msg_types:
getattr(self, msg_type)(msg)
def new_comm(self, target_name, data=None, metadata=None,
comm_id=None, buffers=None):
"""
Create a new Comm, register it, and open its Kernel-side counterpart
Mimics the auto-registration in `Comm.__init__` in the Jupyter Comm.
argument comm_id is optional
"""
comm = Comm(target_name, self.kernel_client, comm_id)
self.register_comm(comm)
try:
comm.open(data, metadata, buffers)
except Exception:
self.unregister_comm(comm)
raise
return comm
def register_target(self, target_name, f):
"""Register a callable f for a given target name
f will be called with two arguments when a comm_open message is
received with `target`:
- the Comm instance
- the `comm_open` message itself.
f can be a Python callable or an import string for one.
"""
if isinstance(f, string_types):
f = import_item(f)
self.targets[target_name] = f
def unregister_target(self, target_name, f):
"""Unregister a callable registered with register_target"""
return self.targets.pop(target_name)
def register_comm(self, comm):
"""Register a new comm"""
comm_id = comm.comm_id
comm.kernel_client = self.kernel_client
self.comms[comm_id] = comm
comm.sig_is_closing.connect(self.unregister_comm)
return comm_id
@QtCore.Slot(object)
def unregister_comm(self, comm):
"""Unregister a comm, and close its counterpart."""
# unlike get_comm, this should raise a KeyError
comm.sig_is_closing.disconnect(self.unregister_comm)
self.comms.pop(comm.comm_id)
def get_comm(self, comm_id, closing=False):
"""Get a comm with a particular id
Returns the comm if found, otherwise None.
This will not raise an error,
it will log messages if the comm cannot be found.
If the comm is closing, it might already have closed,
so this is ignored.
"""
try:
return self.comms[comm_id]
except KeyError:
if closing:
return
self.log.warning("No such comm: %s", comm_id)
# don't create the list of keys if debug messages aren't enabled
if self.log.isEnabledFor(logging.DEBUG):
self.log.debug("Current comms: %s", list(self.comms.keys()))
# comm message handlers
def comm_open(self, msg):
"""Handler for comm_open messages"""
content = msg['content']
comm_id = content['comm_id']
target_name = content['target_name']
f = self.targets.get(target_name, None)
comm = Comm(target_name, self.kernel_client, comm_id)
self.register_comm(comm)
if f is None:
self.log.error("No such comm target registered: %s", target_name)
else:
try:
f(comm, msg)
return
except Exception:
self.log.error("Exception opening comm with target: %s",
target_name, exc_info=True)
# Failure.
try:
comm.close()
except Exception:
self.log.error(
"Could not close comm during `comm_open` failure "
"clean-up. The comm may not have been opened yet.""",
exc_info=True)
def comm_close(self, msg):
"""Handler for comm_close messages"""
content = msg['content']
comm_id = content['comm_id']
comm = self.get_comm(comm_id, closing=True)
if comm is None:
return
self.unregister_comm(comm)
try:
comm.handle_close(msg)
except Exception:
self.log.error('Exception in comm_close for %s', comm_id,
exc_info=True)
def comm_msg(self, msg):
"""Handler for comm_msg messages"""
content = msg['content']
comm_id = content['comm_id']
comm = self.get_comm(comm_id)
if comm is None:
return
try:
comm.handle_msg(msg)
except Exception:
self.log.error('Exception in comm_msg for %s', comm_id,
exc_info=True)
class Comm(MetaQObjectHasTraits(
'NewBase', (LoggingConfigurable, SuperQObject), {})):
"""
Comm base class
"""
sig_is_closing = QtCore.Signal(object)
def __init__(self, target_name, kernel_client, comm_id=None,
msg_callback=None, close_callback=None):
"""
Create a new comm. Must call open to use.
"""
super(Comm, self).__init__(target_name=target_name)
self.target_name = target_name
self.kernel_client = kernel_client
if comm_id is None:
comm_id = uuid.uuid1().hex
self.comm_id = comm_id
self._msg_callback = msg_callback
self._close_callback = close_callback
self._send_channel = self.kernel_client.shell_channel
def _send_msg(self, msg_type, content, data, metadata, buffers):
"""
Send a message on the shell channel.
"""
if data is None:
data = {}
if content is None:
content = {}
content['comm_id'] = self.comm_id
content['data'] = data
msg = self.kernel_client.session.msg(
msg_type, content, metadata=metadata)
if buffers:
msg['buffers'] = buffers
return self._send_channel.send(msg)
# methods for sending messages
def open(self, data=None, metadata=None, buffers=None):
"""Open the kernel-side version of this comm"""
return self._send_msg(
'comm_open', {'target_name': self.target_name},
data, metadata, buffers)
def send(self, data=None, metadata=None, buffers=None):
"""Send a message to the kernel-side version of this comm"""
return self._send_msg(
'comm_msg', {}, data, metadata, buffers)
def close(self, data=None, metadata=None, buffers=None):
"""Close the kernel-side version of this comm"""
self.sig_is_closing.emit(self)
return self._send_msg(
'comm_close', {}, data, metadata, buffers)
# methods for registering callbacks for incoming messages
def on_msg(self, callback):
"""Register a callback for comm_msg
Will be called with the `data` of any comm_msg messages.
Call `on_msg(None)` to disable an existing callback.
"""
self._msg_callback = callback
def on_close(self, callback):
"""Register a callback for comm_close
Will be called with the `data` of the close message.
Call `on_close(None)` to disable an existing callback.
"""
self._close_callback = callback
# methods for handling incoming messages
def handle_msg(self, msg):
"""Handle a comm_msg message"""
self.log.debug("handle_msg[%s](%s)", self.comm_id, msg)
if self._msg_callback:
return self._msg_callback(msg)
def handle_close(self, msg):
"""Handle a comm_close message"""
self.log.debug("handle_close[%s](%s)", self.comm_id, msg)
if self._close_callback:
return self._close_callback(msg)
__all__ = ['CommManager']

View file

@ -0,0 +1,376 @@
"""A navigable completer for the qtconsole"""
# coding : utf-8
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import ipython_genutils.text as text
from qtpy import QtCore, QtGui, QtWidgets
#--------------------------------------------------------------------------
# Return an HTML table with selected item in a special class
#--------------------------------------------------------------------------
def html_tableify(item_matrix, select=None, header=None , footer=None) :
""" returnr a string for an html table"""
if not item_matrix :
return ''
html_cols = []
tds = lambda text : u'<td>'+text+u' </td>'
trs = lambda text : u'<tr>'+text+u'</tr>'
tds_items = [list(map(tds, row)) for row in item_matrix]
if select :
row, col = select
tds_items[row][col] = u'<td class="inverted">'\
+item_matrix[row][col]\
+u' </td>'
#select the right item
html_cols = map(trs, (u''.join(row) for row in tds_items))
head = ''
foot = ''
if header :
head = (u'<tr>'\
+''.join((u'<td>'+header+u'</td>')*len(item_matrix[0]))\
+'</tr>')
if footer :
foot = (u'<tr>'\
+''.join((u'<td>'+footer+u'</td>')*len(item_matrix[0]))\
+'</tr>')
html = (u'<table class="completion" style="white-space:pre"'
'cellspacing=0>' +
head + (u''.join(html_cols)) + foot + u'</table>')
return html
class SlidingInterval(object):
"""a bound interval that follows a cursor
internally used to scoll the completion view when the cursor
try to go beyond the edges, and show '...' when rows are hidden
"""
_min = 0
_max = 1
_current = 0
def __init__(self, maximum=1, width=6, minimum=0, sticky_lenght=1):
"""Create a new bounded interval
any value return by this will be bound between maximum and
minimum. usual width will be 'width', and sticky_length
set when the return interval should expand to max and min
"""
self._min = minimum
self._max = maximum
self._start = 0
self._width = width
self._stop = self._start+self._width+1
self._sticky_lenght = sticky_lenght
@property
def current(self):
"""current cursor position"""
return self._current
@current.setter
def current(self, value):
"""set current cursor position"""
current = min(max(self._min, value), self._max)
self._current = current
if current > self._stop :
self._stop = current
self._start = current-self._width
elif current < self._start :
self._start = current
self._stop = current + self._width
if abs(self._start - self._min) <= self._sticky_lenght :
self._start = self._min
if abs(self._stop - self._max) <= self._sticky_lenght :
self._stop = self._max
@property
def start(self):
"""begiiing of interval to show"""
return self._start
@property
def stop(self):
"""end of interval to show"""
return self._stop
@property
def width(self):
return self._stop - self._start
@property
def nth(self):
return self.current - self.start
class CompletionHtml(QtWidgets.QWidget):
""" A widget for tab completion, navigable by arrow keys """
#--------------------------------------------------------------------------
# 'QObject' interface
#--------------------------------------------------------------------------
_items = ()
_index = (0, 0)
_consecutive_tab = 0
_size = (1, 1)
_old_cursor = None
_start_position = 0
_slice_start = 0
_slice_len = 4
def __init__(self, console_widget):
""" Create a completion widget that is attached to the specified Qt
text edit widget.
"""
assert isinstance(console_widget._control, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
super(CompletionHtml, self).__init__()
self._text_edit = console_widget._control
self._console_widget = console_widget
self._text_edit.installEventFilter(self)
self._sliding_interval = None
self._justified_items = None
# Ensure that the text edit keeps focus when widget is displayed.
self.setFocusProxy(self._text_edit)
def eventFilter(self, obj, event):
""" Reimplemented to handle keyboard input and to auto-hide when the
text edit loses focus.
"""
if obj == self._text_edit:
etype = event.type()
if etype == QtCore.QEvent.KeyPress:
key = event.key()
if self._consecutive_tab == 0 and key in (QtCore.Qt.Key_Tab,):
return False
elif self._consecutive_tab == 1 and key in (QtCore.Qt.Key_Tab,):
# ok , called twice, we grab focus, and show the cursor
self._consecutive_tab = self._consecutive_tab+1
self._update_list()
return True
elif self._consecutive_tab == 2:
if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
self._complete_current()
return True
if key in (QtCore.Qt.Key_Tab,):
self.select_right()
self._update_list()
return True
elif key in ( QtCore.Qt.Key_Down,):
self.select_down()
self._update_list()
return True
elif key in (QtCore.Qt.Key_Right,):
self.select_right()
self._update_list()
return True
elif key in ( QtCore.Qt.Key_Up,):
self.select_up()
self._update_list()
return True
elif key in ( QtCore.Qt.Key_Left,):
self.select_left()
self._update_list()
return True
elif key in ( QtCore.Qt.Key_Escape,):
self.cancel_completion()
return True
else :
self.cancel_completion()
else:
self.cancel_completion()
elif etype == QtCore.QEvent.FocusOut:
self.cancel_completion()
return super(CompletionHtml, self).eventFilter(obj, event)
#--------------------------------------------------------------------------
# 'CompletionHtml' interface
#--------------------------------------------------------------------------
def cancel_completion(self):
"""Cancel the completion
should be called when the completer have to be dismissed
This reset internal variable, clearing the temporary buffer
of the console where the completion are shown.
"""
self._consecutive_tab = 0
self._slice_start = 0
self._console_widget._clear_temporary_buffer()
self._index = (0, 0)
if(self._sliding_interval):
self._sliding_interval = None
#
# ... 2 4 4 4 4 4 4 4 4 4 4 4 4
# 2 2 4 4 4 4 4 4 4 4 4 4 4 4
#
#2 2 x x x x x x x x x x x 5 5
#6 6 x x x x x x x x x x x 5 5
#6 6 x x x x x x x x x x ? 5 5
#6 6 x x x x x x x x x x ? 1 1
#
#3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ...
#3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ...
def _select_index(self, row, col):
"""Change the selection index, and make sure it stays in the right range
A little more complicated than just dooing modulo the number of row columns
to be sure to cycle through all element.
horizontaly, the element are maped like this :
to r <-- a b c d e f --> to g
to f <-- g h i j k l --> to m
to l <-- m n o p q r --> to a
and vertically
a d g j m p
b e h k n q
c f i l o r
"""
nr, nc = self._size
nr = nr-1
nc = nc-1
# case 1
if (row > nr and col >= nc) or (row >= nr and col > nc):
self._select_index(0, 0)
# case 2
elif (row <= 0 and col < 0) or (row < 0 and col <= 0):
self._select_index(nr, nc)
# case 3
elif row > nr :
self._select_index(0, col+1)
# case 4
elif row < 0 :
self._select_index(nr, col-1)
# case 5
elif col > nc :
self._select_index(row+1, 0)
# case 6
elif col < 0 :
self._select_index(row-1, nc)
elif 0 <= row and row <= nr and 0 <= col and col <= nc :
self._index = (row, col)
else :
raise NotImplementedError("you'r trying to go where no completion\
have gone before : %d:%d (%d:%d)"%(row, col, nr, nc) )
@property
def _slice_end(self):
end = self._slice_start+self._slice_len
if end > len(self._items) :
return None
return end
def select_up(self):
"""move cursor up"""
r, c = self._index
self._select_index(r-1, c)
def select_down(self):
"""move cursor down"""
r, c = self._index
self._select_index(r+1, c)
def select_left(self):
"""move cursor left"""
r, c = self._index
self._select_index(r, c-1)
def select_right(self):
"""move cursor right"""
r, c = self._index
self._select_index(r, c+1)
def show_items(self, cursor, items, prefix_length=0):
""" Shows the completion widget with 'items' at the position specified
by 'cursor'.
"""
if not items :
return
# Move cursor to start of the prefix to replace it
# when a item is selected
cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length)
self._start_position = cursor.position()
self._consecutive_tab = 1
# Calculate the number of characters available.
width = self._text_edit.document().textWidth()
char_width = self._console_widget._get_font_width()
displaywidth = int(max(10, (width / char_width) - 1))
items_m, ci = text.compute_item_matrix(items, empty=' ',
displaywidth=displaywidth)
self._sliding_interval = SlidingInterval(len(items_m)-1)
self._items = items_m
self._size = (ci['rows_numbers'], ci['columns_numbers'])
self._old_cursor = cursor
self._index = (0, 0)
sjoin = lambda x : [ y.ljust(w, ' ') for y, w in zip(x, ci['columns_width'])]
self._justified_items = list(map(sjoin, items_m))
self._update_list(hilight=False)
def _update_list(self, hilight=True):
""" update the list of completion and hilight the currently selected completion """
self._sliding_interval.current = self._index[0]
head = None
foot = None
if self._sliding_interval.start > 0 :
head = '...'
if self._sliding_interval.stop < self._sliding_interval._max:
foot = '...'
items_m = self._justified_items[\
self._sliding_interval.start:\
self._sliding_interval.stop+1\
]
self._console_widget._clear_temporary_buffer()
if(hilight):
sel = (self._sliding_interval.nth, self._index[1])
else :
sel = None
strng = html_tableify(items_m, select=sel, header=head, footer=foot)
self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True)
#--------------------------------------------------------------------------
# Protected interface
#--------------------------------------------------------------------------
def _complete_current(self):
""" Perform the completion with the currently selected item.
"""
i = self._index
item = self._items[i[0]][i[1]]
item = item.strip()
if item :
self._current_text_cursor().insertText(item)
self.cancel_completion()
def _current_text_cursor(self):
""" Returns a cursor with text between the start position and the
current position selected.
"""
cursor = self._text_edit.textCursor()
if cursor.position() >= self._start_position:
cursor.setPosition(self._start_position,
QtGui.QTextCursor.KeepAnchor)
return cursor

View file

@ -0,0 +1,60 @@
"""A simple completer for the qtconsole"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from qtpy import QtCore, QtGui, QtWidgets
import ipython_genutils.text as text
class CompletionPlain(QtWidgets.QWidget):
""" A widget for tab completion, navigable by arrow keys """
#--------------------------------------------------------------------------
# 'QObject' interface
#--------------------------------------------------------------------------
def __init__(self, console_widget):
""" Create a completion widget that is attached to the specified Qt
text edit widget.
"""
assert isinstance(console_widget._control, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
super(CompletionPlain, self).__init__()
self._text_edit = console_widget._control
self._console_widget = console_widget
self._text_edit.installEventFilter(self)
def eventFilter(self, obj, event):
""" Reimplemented to handle keyboard input and to auto-hide when the
text edit loses focus.
"""
if obj == self._text_edit:
etype = event.type()
if etype in( QtCore.QEvent.KeyPress, QtCore.QEvent.FocusOut ):
self.cancel_completion()
return super(CompletionPlain, self).eventFilter(obj, event)
#--------------------------------------------------------------------------
# 'CompletionPlain' interface
#--------------------------------------------------------------------------
def cancel_completion(self):
"""Cancel the completion, reseting internal variable, clearing buffer """
self._console_widget._clear_temporary_buffer()
def show_items(self, cursor, items, prefix_length=0):
""" Shows the completion widget with 'items' at the position specified
by 'cursor'.
"""
if not items :
return
self.cancel_completion()
strng = text.columnize(items)
# Move cursor to start of the prefix to replace it
# when a item is selected
cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length)
self._console_widget._fill_temporary_buffer(cursor, strng, html=False)

View file

@ -0,0 +1,214 @@
"""A dropdown completer widget for the qtconsole."""
import os
import sys
from qtpy import QtCore, QtGui, QtWidgets
class CompletionWidget(QtWidgets.QListWidget):
""" A widget for GUI tab completion.
"""
#--------------------------------------------------------------------------
# 'QObject' interface
#--------------------------------------------------------------------------
def __init__(self, console_widget):
""" Create a completion widget that is attached to the specified Qt
text edit widget.
"""
text_edit = console_widget._control
assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
super(CompletionWidget, self).__init__(parent=console_widget)
self._text_edit = text_edit
self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
# We need Popup style to ensure correct mouse interaction
# (dialog would dissappear on mouse click with ToolTip style)
self.setWindowFlags(QtCore.Qt.Popup)
self.setAttribute(QtCore.Qt.WA_StaticContents)
original_policy = text_edit.focusPolicy()
self.setFocusPolicy(QtCore.Qt.NoFocus)
text_edit.setFocusPolicy(original_policy)
# Ensure that the text edit keeps focus when widget is displayed.
self.setFocusProxy(self._text_edit)
self.setFrameShadow(QtWidgets.QFrame.Plain)
self.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.itemActivated.connect(self._complete_current)
def eventFilter(self, obj, event):
""" Reimplemented to handle mouse input and to auto-hide when the
text edit loses focus.
"""
if obj is self:
if event.type() == QtCore.QEvent.MouseButtonPress:
pos = self.mapToGlobal(event.pos())
target = QtWidgets.QApplication.widgetAt(pos)
if (target and self.isAncestorOf(target) or target is self):
return False
else:
self.cancel_completion()
return super(CompletionWidget, self).eventFilter(obj, event)
def keyPressEvent(self, event):
key = event.key()
if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Tab):
self._complete_current()
elif key == QtCore.Qt.Key_Escape:
self.hide()
elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
return super(CompletionWidget, self).keyPressEvent(event)
else:
QtWidgets.QApplication.sendEvent(self._text_edit, event)
#--------------------------------------------------------------------------
# 'QWidget' interface
#--------------------------------------------------------------------------
def hideEvent(self, event):
""" Reimplemented to disconnect signal handlers and event filter.
"""
super(CompletionWidget, self).hideEvent(event)
try:
self._text_edit.cursorPositionChanged.disconnect(self._update_current)
except TypeError:
pass
self.removeEventFilter(self)
def showEvent(self, event):
""" Reimplemented to connect signal handlers and event filter.
"""
super(CompletionWidget, self).showEvent(event)
self._text_edit.cursorPositionChanged.connect(self._update_current)
self.installEventFilter(self)
#--------------------------------------------------------------------------
# 'CompletionWidget' interface
#--------------------------------------------------------------------------
def show_items(self, cursor, items, prefix_length=0):
""" Shows the completion widget with 'items' at the position specified
by 'cursor'.
"""
text_edit = self._text_edit
point = self._get_top_left_position(cursor)
self.clear()
path_items = []
for item in items:
# Check if the item could refer to a file or dir. The replacing
# of '"' is needed for items on Windows
if (os.path.isfile(os.path.abspath(item.replace("\"", ""))) or
os.path.isdir(os.path.abspath(item.replace("\"", "")))):
path_items.append(item.replace("\"", ""))
else:
list_item = QtWidgets.QListWidgetItem()
list_item.setData(QtCore.Qt.UserRole, item)
# Need to split to only show last element of a dot completion
list_item.setText(item.split(".")[-1])
self.addItem(list_item)
common_prefix = os.path.dirname(os.path.commonprefix(path_items))
for path_item in path_items:
list_item = QtWidgets.QListWidgetItem()
list_item.setData(QtCore.Qt.UserRole, path_item)
if common_prefix:
text = path_item.split(common_prefix)[-1]
else:
text = path_item
list_item.setText(text)
self.addItem(list_item)
height = self.sizeHint().height()
screen_rect = QtWidgets.QApplication.desktop().availableGeometry(self)
if (screen_rect.size().height() + screen_rect.y() -
point.y() - height < 0):
point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
point.setY(point.y() - height)
w = (self.sizeHintForColumn(0) +
self.verticalScrollBar().sizeHint().width() +
2 * self.frameWidth())
self.setGeometry(point.x(), point.y(), w, height)
# Move cursor to start of the prefix to replace it
# when a item is selected
cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length)
self._start_position = cursor.position()
self.setCurrentRow(0)
self.raise_()
self.show()
#--------------------------------------------------------------------------
# Protected interface
#--------------------------------------------------------------------------
def _get_top_left_position(self, cursor):
""" Get top left position for this widget.
"""
point = self._text_edit.cursorRect(cursor).center()
point_size = self._text_edit.font().pointSize()
if sys.platform == 'darwin':
delta = int((point_size * 1.20) ** 0.98)
elif os.name == 'nt':
delta = int((point_size * 1.20) ** 1.05)
else:
delta = int((point_size * 1.20) ** 0.98)
y = delta - (point_size / 2)
point.setY(point.y() + y)
point = self._text_edit.mapToGlobal(point)
return point
def _complete_current(self):
""" Perform the completion with the currently selected item.
"""
text = self.currentItem().data(QtCore.Qt.UserRole)
self._current_text_cursor().insertText(text)
self.hide()
def _current_text_cursor(self):
""" Returns a cursor with text between the start position and the
current position selected.
"""
cursor = self._text_edit.textCursor()
if cursor.position() >= self._start_position:
cursor.setPosition(self._start_position,
QtGui.QTextCursor.KeepAnchor)
return cursor
def _update_current(self):
""" Updates the current item based on the current text and the
position of the widget.
"""
# Update widget position
cursor = self._text_edit.textCursor()
point = self._get_top_left_position(cursor)
self.move(point)
# Update current item
prefix = self._current_text_cursor().selection().toPlainText()
if prefix:
items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
QtCore.Qt.MatchCaseSensitive))
if items:
self.setCurrentItem(items[0])
else:
self.hide()
else:
self.hide()
def cancel_completion(self):
self.hide()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,793 @@
"""Frontend widget for the Qt Console"""
# Copyright (c) Jupyter Development Team
# Distributed under the terms of the Modified BSD License.
from __future__ import print_function
from collections import namedtuple
import sys
import uuid
import re
from qtpy import QtCore, QtGui, QtWidgets
from ipython_genutils import py3compat
from ipython_genutils.importstring import import_item
from qtconsole.base_frontend_mixin import BaseFrontendMixin
from traitlets import Any, Bool, Instance, Unicode, DottedObjectName, default
from .bracket_matcher import BracketMatcher
from .call_tip_widget import CallTipWidget
from .history_console_widget import HistoryConsoleWidget
from .pygments_highlighter import PygmentsHighlighter
class FrontendHighlighter(PygmentsHighlighter):
""" A PygmentsHighlighter that understands and ignores prompts.
"""
def __init__(self, frontend, lexer=None):
super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
self._current_offset = 0
self._frontend = frontend
self.highlighting_on = False
self._classic_prompt_re = re.compile(
r'^(%s)?([ \t]*>>> |^[ \t]*\.\.\. )' % re.escape(frontend.other_output_prefix)
)
self._ipy_prompt_re = re.compile(
r'^(%s)?([ \t]*In \[\d+\]: |[ \t]*\ \ \ \.\.\.+: )' % re.escape(frontend.other_output_prefix)
)
def transform_classic_prompt(self, line):
"""Handle inputs that start with '>>> ' syntax."""
if not line or line.isspace():
return line
m = self._classic_prompt_re.match(line)
if m:
return line[len(m.group(0)):]
else:
return line
def transform_ipy_prompt(self, line):
"""Handle inputs that start classic IPython prompt syntax."""
if not line or line.isspace():
return line
m = self._ipy_prompt_re.match(line)
if m:
return line[len(m.group(0)):]
else:
return line
def highlightBlock(self, string):
""" Highlight a block of text. Reimplemented to highlight selectively.
"""
if not hasattr(self, 'highlighting_on') or not self.highlighting_on:
return
# The input to this function is a unicode string that may contain
# paragraph break characters, non-breaking spaces, etc. Here we acquire
# the string as plain text so we can compare it.
current_block = self.currentBlock()
string = self._frontend._get_block_plain_text(current_block)
# Only highlight if we can identify a prompt, but make sure not to
# highlight the prompt.
without_prompt = self.transform_ipy_prompt(string)
diff = len(string) - len(without_prompt)
if diff > 0:
self._current_offset = diff
super(FrontendHighlighter, self).highlightBlock(without_prompt)
def rehighlightBlock(self, block):
""" Reimplemented to temporarily enable highlighting if disabled.
"""
old = self.highlighting_on
self.highlighting_on = True
super(FrontendHighlighter, self).rehighlightBlock(block)
self.highlighting_on = old
def setFormat(self, start, count, format):
""" Reimplemented to highlight selectively.
"""
start += self._current_offset
super(FrontendHighlighter, self).setFormat(start, count, format)
class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
""" A Qt frontend for a generic Python kernel.
"""
# The text to show when the kernel is (re)started.
banner = Unicode(config=True)
kernel_banner = Unicode()
# Whether to show the banner
_display_banner = Bool(False)
# An option and corresponding signal for overriding the default kernel
# interrupt behavior.
custom_interrupt = Bool(False)
custom_interrupt_requested = QtCore.Signal()
# An option and corresponding signals for overriding the default kernel
# restart behavior.
custom_restart = Bool(False)
custom_restart_kernel_died = QtCore.Signal(float)
custom_restart_requested = QtCore.Signal()
# Whether to automatically show calltips on open-parentheses.
enable_calltips = Bool(True, config=True,
help="Whether to draw information calltips on open-parentheses.")
clear_on_kernel_restart = Bool(True, config=True,
help="Whether to clear the console when the kernel is restarted")
confirm_restart = Bool(True, config=True,
help="Whether to ask for user confirmation when restarting kernel")
lexer_class = DottedObjectName(config=True,
help="The pygments lexer class to use."
)
def _lexer_class_changed(self, name, old, new):
lexer_class = import_item(new)
self.lexer = lexer_class()
def _lexer_class_default(self):
if py3compat.PY3:
return 'pygments.lexers.Python3Lexer'
else:
return 'pygments.lexers.PythonLexer'
lexer = Any()
def _lexer_default(self):
lexer_class = import_item(self.lexer_class)
return lexer_class()
# Emitted when a user visible 'execute_request' has been submitted to the
# kernel from the FrontendWidget. Contains the code to be executed.
executing = QtCore.Signal(object)
# Emitted when a user-visible 'execute_reply' has been received from the
# kernel and processed by the FrontendWidget. Contains the response message.
executed = QtCore.Signal(object)
# Emitted when an exit request has been received from the kernel.
exit_requested = QtCore.Signal(object)
_CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
_CompletionRequest = namedtuple('_CompletionRequest',
['id', 'code', 'pos'])
_ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
_local_kernel = False
_highlighter = Instance(FrontendHighlighter, allow_none=True)
# -------------------------------------------------------------------------
# 'Object' interface
# -------------------------------------------------------------------------
def __init__(self, local_kernel=_local_kernel, *args, **kw):
super(FrontendWidget, self).__init__(*args, **kw)
# FrontendWidget protected variables.
self._bracket_matcher = BracketMatcher(self._control)
self._call_tip_widget = CallTipWidget(self._control)
self._copy_raw_action = QtWidgets.QAction('Copy (Raw Text)', None)
self._hidden = False
self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
self._kernel_manager = None
self._kernel_client = None
self._request_info = {}
self._request_info['execute'] = {}
self._callback_dict = {}
self._display_banner = True
# Configure the ConsoleWidget.
self.tab_width = 4
self._set_continuation_prompt('... ')
# Configure the CallTipWidget.
self._call_tip_widget.setFont(self.font)
self.font_changed.connect(self._call_tip_widget.setFont)
# Configure actions.
action = self._copy_raw_action
key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
action.setEnabled(False)
action.setShortcut(QtGui.QKeySequence(key))
action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
action.triggered.connect(self.copy_raw)
self.copy_available.connect(action.setEnabled)
self.addAction(action)
# Connect signal handlers.
document = self._control.document()
document.contentsChange.connect(self._document_contents_change)
# Set flag for whether we are connected via localhost.
self._local_kernel = local_kernel
# Whether or not a clear_output call is pending new output.
self._pending_clearoutput = False
#---------------------------------------------------------------------------
# 'ConsoleWidget' public interface
#---------------------------------------------------------------------------
def copy(self):
""" Copy the currently selected text to the clipboard, removing prompts.
"""
if self._page_control is not None and self._page_control.hasFocus():
self._page_control.copy()
elif self._control.hasFocus():
text = self._control.textCursor().selection().toPlainText()
if text:
# Remove prompts.
lines = text.splitlines()
lines = map(self._highlighter.transform_classic_prompt, lines)
lines = map(self._highlighter.transform_ipy_prompt, lines)
text = '\n'.join(lines)
# Needed to prevent errors when copying the prompt.
# See issue 264
try:
was_newline = text[-1] == '\n'
except IndexError:
was_newline = False
if was_newline: # user doesn't need newline
text = text[:-1]
QtWidgets.QApplication.clipboard().setText(text)
else:
self.log.debug("frontend widget : unknown copy target")
#---------------------------------------------------------------------------
# 'ConsoleWidget' abstract interface
#---------------------------------------------------------------------------
def _execute(self, source, hidden):
""" Execute 'source'. If 'hidden', do not show any output.
See parent class :meth:`execute` docstring for full details.
"""
msg_id = self.kernel_client.execute(source, hidden)
self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
self._hidden = hidden
if not hidden:
self.executing.emit(source)
def _prompt_started_hook(self):
""" Called immediately after a new prompt is displayed.
"""
if not self._reading:
self._highlighter.highlighting_on = True
def _prompt_finished_hook(self):
""" Called immediately after a prompt is finished, i.e. when some input
will be processed and a new prompt displayed.
"""
if not self._reading:
self._highlighter.highlighting_on = False
def _tab_pressed(self):
""" Called when the tab key is pressed. Returns whether to continue
processing the event.
"""
# Perform tab completion if:
# 1) The cursor is in the input buffer.
# 2) There is a non-whitespace character before the cursor.
# 3) There is no active selection.
text = self._get_input_buffer_cursor_line()
if text is None:
return False
non_ws_before = bool(text[:self._get_input_buffer_cursor_column()].strip())
complete = non_ws_before and self._get_cursor().selectedText() == ''
if complete:
self._complete()
return not complete
#---------------------------------------------------------------------------
# 'ConsoleWidget' protected interface
#---------------------------------------------------------------------------
def _context_menu_make(self, pos):
""" Reimplemented to add an action for raw copy.
"""
menu = super(FrontendWidget, self)._context_menu_make(pos)
for before_action in menu.actions():
if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
QtGui.QKeySequence.ExactMatch:
menu.insertAction(before_action, self._copy_raw_action)
break
return menu
def request_interrupt_kernel(self):
if self._executing:
self.interrupt_kernel()
def request_restart_kernel(self):
message = 'Are you sure you want to restart the kernel?'
self.restart_kernel(message, now=False)
def _event_filter_console_keypress(self, event):
""" Reimplemented for execution interruption and smart backspace.
"""
key = event.key()
if self._control_key_down(event.modifiers(), include_command=False):
if key == QtCore.Qt.Key_C and self._executing:
self.request_interrupt_kernel()
return True
elif key == QtCore.Qt.Key_Period:
self.request_restart_kernel()
return True
elif not event.modifiers() & QtCore.Qt.AltModifier:
# Smart backspace: remove four characters in one backspace if:
# 1) everything left of the cursor is whitespace
# 2) the four characters immediately left of the cursor are spaces
if key == QtCore.Qt.Key_Backspace:
col = self._get_input_buffer_cursor_column()
cursor = self._control.textCursor()
if col > 3 and not cursor.hasSelection():
text = self._get_input_buffer_cursor_line()[:col]
if text.endswith(' ') and not text.strip():
cursor.movePosition(QtGui.QTextCursor.Left,
QtGui.QTextCursor.KeepAnchor, 4)
cursor.removeSelectedText()
return True
return super(FrontendWidget, self)._event_filter_console_keypress(event)
#---------------------------------------------------------------------------
# 'BaseFrontendMixin' abstract interface
#---------------------------------------------------------------------------
def _handle_clear_output(self, msg):
"""Handle clear output messages."""
if self.include_output(msg):
wait = msg['content'].get('wait', True)
if wait:
self._pending_clearoutput = True
else:
self.clear_output()
def _silent_exec_callback(self, expr, callback):
"""Silently execute `expr` in the kernel and call `callback` with reply
the `expr` is evaluated silently in the kernel (without) output in
the frontend. Call `callback` with the
`repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
Parameters
----------
expr : string
valid string to be executed by the kernel.
callback : function
function accepting one argument, as a string. The string will be
the `repr` of the result of evaluating `expr`
The `callback` is called with the `repr()` of the result of `expr` as
first argument. To get the object, do `eval()` on the passed value.
See Also
--------
_handle_exec_callback : private method, deal with calling callback with reply
"""
# generate uuid, which would be used as an indication of whether or
# not the unique request originated from here (can use msg id ?)
local_uuid = str(uuid.uuid1())
msg_id = self.kernel_client.execute('',
silent=True, user_expressions={ local_uuid:expr })
self._callback_dict[local_uuid] = callback
self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
def _handle_exec_callback(self, msg):
"""Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
Parameters
----------
msg : raw message send by the kernel containing an `user_expressions`
and having a 'silent_exec_callback' kind.
Notes
-----
This function will look for a `callback` associated with the
corresponding message id. Association has been made by
`_silent_exec_callback`. `callback` is then called with the `repr()`
of the value of corresponding `user_expressions` as argument.
`callback` is then removed from the known list so that any message
coming again with the same id won't trigger it.
"""
user_exp = msg['content'].get('user_expressions')
if not user_exp:
return
for expression in user_exp:
if expression in self._callback_dict:
self._callback_dict.pop(expression)(user_exp[expression])
def _handle_execute_reply(self, msg):
""" Handles replies for code execution.
"""
self.log.debug("execute_reply: %s", msg.get('content', ''))
msg_id = msg['parent_header']['msg_id']
info = self._request_info['execute'].get(msg_id)
# unset reading flag, because if execute finished, raw_input can't
# still be pending.
self._reading = False
# Note: If info is NoneType, this is ignored
if info and info.kind == 'user' and not self._hidden:
# Make sure that all output from the SUB channel has been processed
# before writing a new prompt.
self.kernel_client.iopub_channel.flush()
# Reset the ANSI style information to prevent bad text in stdout
# from messing up our colors. We're not a true terminal so we're
# allowed to do this.
if self.ansi_codes:
self._ansi_processor.reset_sgr()
content = msg['content']
status = content['status']
if status == 'ok':
self._process_execute_ok(msg)
elif status == 'aborted':
self._process_execute_abort(msg)
self._show_interpreter_prompt_for_reply(msg)
self.executed.emit(msg)
self._request_info['execute'].pop(msg_id)
elif info and info.kind == 'silent_exec_callback' and not self._hidden:
self._handle_exec_callback(msg)
self._request_info['execute'].pop(msg_id)
elif info and not self._hidden:
raise RuntimeError("Unknown handler for %s" % info.kind)
def _handle_error(self, msg):
""" Handle error messages.
"""
self._process_execute_error(msg)
def _handle_input_request(self, msg):
""" Handle requests for raw_input.
"""
self.log.debug("input: %s", msg.get('content', ''))
if self._hidden:
raise RuntimeError('Request for raw input during hidden execution.')
# Make sure that all output from the SUB channel has been processed
# before entering readline mode.
self.kernel_client.iopub_channel.flush()
def callback(line):
self._finalize_input_request()
self.kernel_client.input(line)
if self._reading:
self.log.debug("Got second input request, assuming first was interrupted.")
self._reading = False
self._readline(msg['content']['prompt'], callback=callback, password=msg['content']['password'])
def _kernel_restarted_message(self, died=True):
msg = "Kernel died, restarting" if died else "Kernel restarting"
self._append_html("<br>%s<hr><br>" % msg,
before_prompt=False
)
def _handle_kernel_died(self, since_last_heartbeat):
"""Handle the kernel's death (if we do not own the kernel).
"""
self.log.warning("kernel died: %s", since_last_heartbeat)
if self.custom_restart:
self.custom_restart_kernel_died.emit(since_last_heartbeat)
else:
self._kernel_restarted_message(died=True)
self.reset()
def _handle_kernel_restarted(self, died=True):
"""Notice that the autorestarter restarted the kernel.
There's nothing to do but show a message.
"""
self.log.warning("kernel restarted")
self._kernel_restarted_message(died=died)
self.reset()
def _handle_inspect_reply(self, rep):
"""Handle replies for call tips."""
self.log.debug("oinfo: %s", rep.get('content', ''))
cursor = self._get_cursor()
info = self._request_info.get('call_tip')
if info and info.id == rep['parent_header']['msg_id'] and \
info.pos == cursor.position():
content = rep['content']
if content.get('status') == 'ok' and content.get('found', False):
self._call_tip_widget.show_inspect_data(content)
def _handle_execute_result(self, msg):
""" Handle display hook output.
"""
self.log.debug("execute_result: %s", msg.get('content', ''))
if self.include_output(msg):
self.flush_clearoutput()
text = msg['content']['data']
self._append_plain_text(text + '\n', before_prompt=True)
def _handle_stream(self, msg):
""" Handle stdout, stderr, and stdin.
"""
self.log.debug("stream: %s", msg.get('content', ''))
if self.include_output(msg):
self.flush_clearoutput()
self.append_stream(msg['content']['text'])
def _handle_shutdown_reply(self, msg):
""" Handle shutdown signal, only if from other console.
"""
self.log.debug("shutdown: %s", msg.get('content', ''))
restart = msg.get('content', {}).get('restart', False)
if not self._hidden and not self.from_here(msg):
# got shutdown reply, request came from session other than ours
if restart:
# someone restarted the kernel, handle it
self._handle_kernel_restarted(died=False)
else:
# kernel was shutdown permanently
# this triggers exit_requested if the kernel was local,
# and a dialog if the kernel was remote,
# so we don't suddenly clear the qtconsole without asking.
if self._local_kernel:
self.exit_requested.emit(self)
else:
title = self.window().windowTitle()
reply = QtWidgets.QMessageBox.question(self, title,
"Kernel has been shutdown permanently. "
"Close the Console?",
QtWidgets.QMessageBox.Yes,QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
self.exit_requested.emit(self)
def _handle_status(self, msg):
"""Handle status message"""
# This is where a busy/idle indicator would be triggered,
# when we make one.
state = msg['content'].get('execution_state', '')
if state == 'starting':
# kernel started while we were running
if self._executing:
self._handle_kernel_restarted(died=True)
elif state == 'idle':
pass
elif state == 'busy':
pass
def _started_channels(self):
""" Called when the KernelManager channels have started listening or
when the frontend is assigned an already listening KernelManager.
"""
self.reset(clear=True)
#---------------------------------------------------------------------------
# 'FrontendWidget' public interface
#---------------------------------------------------------------------------
def copy_raw(self):
""" Copy the currently selected text to the clipboard without attempting
to remove prompts or otherwise alter the text.
"""
self._control.copy()
def interrupt_kernel(self):
""" Attempts to interrupt the running kernel.
Also unsets _reading flag, to avoid runtime errors
if raw_input is called again.
"""
if self.custom_interrupt:
self._reading = False
self.custom_interrupt_requested.emit()
elif self.kernel_manager:
self._reading = False
self.kernel_manager.interrupt_kernel()
else:
self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
def reset(self, clear=False):
""" Resets the widget to its initial state if ``clear`` parameter
is True, otherwise
prints a visual indication of the fact that the kernel restarted, but
does not clear the traces from previous usage of the kernel before it
was restarted. With ``clear=True``, it is similar to ``%clear``, but
also re-writes the banner and aborts execution if necessary.
"""
if self._executing:
self._executing = False
self._request_info['execute'] = {}
self._reading = False
self._highlighter.highlighting_on = False
if clear:
self._control.clear()
if self._display_banner:
self._append_plain_text(self.banner)
if self.kernel_banner:
self._append_plain_text(self.kernel_banner)
# update output marker for stdout/stderr, so that startup
# messages appear after banner:
self._show_interpreter_prompt()
def restart_kernel(self, message, now=False):
""" Attempts to restart the running kernel.
"""
# FIXME: now should be configurable via a checkbox in the dialog. Right
# now at least the heartbeat path sets it to True and the manual restart
# to False. But those should just be the pre-selected states of a
# checkbox that the user could override if so desired. But I don't know
# enough Qt to go implementing the checkbox now.
if self.custom_restart:
self.custom_restart_requested.emit()
return
if self.kernel_manager:
# Pause the heart beat channel to prevent further warnings.
self.kernel_client.hb_channel.pause()
# Prompt the user to restart the kernel. Un-pause the heartbeat if
# they decline. (If they accept, the heartbeat will be un-paused
# automatically when the kernel is restarted.)
if self.confirm_restart:
buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
result = QtWidgets.QMessageBox.question(self, 'Restart kernel?',
message, buttons)
do_restart = result == QtWidgets.QMessageBox.Yes
else:
# confirm_restart is False, so we don't need to ask user
# anything, just do the restart
do_restart = True
if do_restart:
try:
self.kernel_manager.restart_kernel(now=now)
except RuntimeError as e:
self._append_plain_text(
'Error restarting kernel: %s\n' % e,
before_prompt=True
)
else:
self._append_html("<br>Restarting kernel...\n<hr><br>",
before_prompt=True,
)
else:
self.kernel_client.hb_channel.unpause()
else:
self._append_plain_text(
'Cannot restart a Kernel I did not start\n',
before_prompt=True
)
def append_stream(self, text):
"""Appends text to the output stream."""
self._append_plain_text(text, before_prompt=True)
def flush_clearoutput(self):
"""If a clearoutput is pending, execute it."""
if self._pending_clearoutput:
self._pending_clearoutput = False
self.clear_output()
def clear_output(self):
"""Clears the current line of output."""
cursor = self._control.textCursor()
cursor.beginEditBlock()
cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
cursor.insertText('')
cursor.endEditBlock()
#---------------------------------------------------------------------------
# 'FrontendWidget' protected interface
#---------------------------------------------------------------------------
def _auto_call_tip(self):
"""Trigger call tip automatically on open parenthesis
Call tips can be requested explcitly with `_call_tip`.
"""
cursor = self._get_cursor()
cursor.movePosition(QtGui.QTextCursor.Left)
if cursor.document().characterAt(cursor.position()) == '(':
# trigger auto call tip on open paren
self._call_tip()
def _call_tip(self):
"""Shows a call tip, if appropriate, at the current cursor location."""
# Decide if it makes sense to show a call tip
if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
return False
cursor_pos = self._get_input_buffer_cursor_pos()
code = self.input_buffer
# Send the metadata request to the kernel
msg_id = self.kernel_client.inspect(code, cursor_pos)
pos = self._get_cursor().position()
self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
return True
def _complete(self):
""" Performs completion at the current cursor location.
"""
code = self.input_buffer
cursor_pos = self._get_input_buffer_cursor_pos()
# Send the completion request to the kernel
msg_id = self.kernel_client.complete(code=code, cursor_pos=cursor_pos)
info = self._CompletionRequest(msg_id, code, cursor_pos)
self._request_info['complete'] = info
def _process_execute_abort(self, msg):
""" Process a reply for an aborted execution request.
"""
self._append_plain_text("ERROR: execution aborted\n")
def _process_execute_error(self, msg):
""" Process a reply for an execution request that resulted in an error.
"""
content = msg['content']
# If a SystemExit is passed along, this means exit() was called - also
# all the ipython %exit magic syntax of '-k' to be used to keep
# the kernel running
if content['ename']=='SystemExit':
keepkernel = content['evalue']=='-k' or content['evalue']=='True'
self._keep_kernel_on_exit = keepkernel
self.exit_requested.emit(self)
else:
traceback = ''.join(content['traceback'])
self._append_plain_text(traceback)
def _process_execute_ok(self, msg):
""" Process a reply for a successful execution request.
"""
payload = msg['content'].get('payload', [])
for item in payload:
if not self._process_execute_payload(item):
warning = 'Warning: received unknown payload of type %s'
print(warning % repr(item['source']))
def _process_execute_payload(self, item):
""" Process a single payload item from the list of payload items in an
execution reply. Returns whether the payload was handled.
"""
# The basic FrontendWidget doesn't handle payloads, as they are a
# mechanism for going beyond the standard Python interpreter model.
return False
def _show_interpreter_prompt(self):
""" Shows a prompt for the interpreter.
"""
self._show_prompt('>>> ')
def _show_interpreter_prompt_for_reply(self, msg):
""" Shows a prompt for the interpreter given an 'execute_reply' message.
"""
self._show_interpreter_prompt()
#------ Signal handlers ----------------------------------------------------
def _document_contents_change(self, position, removed, added):
""" Called whenever the document's content changes. Display a call tip
if appropriate.
"""
# Calculate where the cursor should be *after* the change:
position += added
document = self._control.document()
if position == self._get_cursor().position():
self._auto_call_tip()
#------ Trait default initializers -----------------------------------------
@default('banner')
def _banner_default(self):
""" Returns the standard Python banner.
"""
banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
'"license" for more information.'
return banner % (sys.version, sys.platform)

View file

@ -0,0 +1,261 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from qtpy import QtGui
from ipython_genutils.py3compat import unicode_type
from traitlets import Bool
from .console_widget import ConsoleWidget
class HistoryConsoleWidget(ConsoleWidget):
""" A ConsoleWidget that keeps a history of the commands that have been
executed and provides a readline-esque interface to this history.
"""
#------ Configuration ------------------------------------------------------
# If enabled, the input buffer will become "locked" to history movement when
# an edit is made to a multi-line input buffer. To override the lock, use
# Shift in conjunction with the standard history cycling keys.
history_lock = Bool(False, config=True)
#---------------------------------------------------------------------------
# 'object' interface
#---------------------------------------------------------------------------
def __init__(self, *args, **kw):
super(HistoryConsoleWidget, self).__init__(*args, **kw)
# HistoryConsoleWidget protected variables.
self._history = []
self._history_edits = {}
self._history_index = 0
self._history_prefix = ''
#---------------------------------------------------------------------------
# 'ConsoleWidget' public interface
#---------------------------------------------------------------------------
def do_execute(self, source, complete, indent):
""" Reimplemented to the store history. """
history = self.input_buffer if source is None else source
super(HistoryConsoleWidget, self).do_execute(source, complete, indent)
if complete:
# Save the command unless it was an empty string or was identical
# to the previous command.
history = history.rstrip()
if history and (not self._history or self._history[-1] != history):
self._history.append(history)
# Emulate readline: reset all history edits.
self._history_edits = {}
# Move the history index to the most recent item.
self._history_index = len(self._history)
#---------------------------------------------------------------------------
# 'ConsoleWidget' abstract interface
#---------------------------------------------------------------------------
def _up_pressed(self, shift_modifier):
""" Called when the up key is pressed. Returns whether to continue
processing the event.
"""
prompt_cursor = self._get_prompt_cursor()
if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
# Bail out if we're locked.
if self._history_locked() and not shift_modifier:
return False
# Set a search prefix based on the cursor position.
pos = self._get_input_buffer_cursor_pos()
input_buffer = self.input_buffer
# use the *shortest* of the cursor column and the history prefix
# to determine if the prefix has changed
n = min(pos, len(self._history_prefix))
# prefix changed, restart search from the beginning
if (self._history_prefix[:n] != input_buffer[:n]):
self._history_index = len(self._history)
# the only time we shouldn't set the history prefix
# to the line up to the cursor is if we are already
# in a simple scroll (no prefix),
# and the cursor is at the end of the first line
# check if we are at the end of the first line
c = self._get_cursor()
current_pos = c.position()
c.movePosition(QtGui.QTextCursor.EndOfBlock)
at_eol = (c.position() == current_pos)
if self._history_index == len(self._history) or \
not (self._history_prefix == '' and at_eol) or \
not (self._get_edited_history(self._history_index)[:pos] == input_buffer[:pos]):
self._history_prefix = input_buffer[:pos]
# Perform the search.
self.history_previous(self._history_prefix,
as_prefix=not shift_modifier)
# Go to the first line of the prompt for seemless history scrolling.
# Emulate readline: keep the cursor position fixed for a prefix
# search.
cursor = self._get_prompt_cursor()
if self._history_prefix:
cursor.movePosition(QtGui.QTextCursor.Right,
n=len(self._history_prefix))
else:
cursor.movePosition(QtGui.QTextCursor.EndOfBlock)
self._set_cursor(cursor)
return False
return True
def _down_pressed(self, shift_modifier):
""" Called when the down key is pressed. Returns whether to continue
processing the event.
"""
end_cursor = self._get_end_cursor()
if self._get_cursor().blockNumber() == end_cursor.blockNumber():
# Bail out if we're locked.
if self._history_locked() and not shift_modifier:
return False
# Perform the search.
replaced = self.history_next(self._history_prefix,
as_prefix=not shift_modifier)
# Emulate readline: keep the cursor position fixed for a prefix
# search. (We don't need to move the cursor to the end of the buffer
# in the other case because this happens automatically when the
# input buffer is set.)
if self._history_prefix and replaced:
cursor = self._get_prompt_cursor()
cursor.movePosition(QtGui.QTextCursor.Right,
n=len(self._history_prefix))
self._set_cursor(cursor)
return False
return True
#---------------------------------------------------------------------------
# 'HistoryConsoleWidget' public interface
#---------------------------------------------------------------------------
def history_previous(self, substring='', as_prefix=True):
""" If possible, set the input buffer to a previous history item.
Parameters
----------
substring : str, optional
If specified, search for an item with this substring.
as_prefix : bool, optional
If True, the substring must match at the beginning (default).
Returns
-------
Whether the input buffer was changed.
"""
index = self._history_index
replace = False
while index > 0:
index -= 1
history = self._get_edited_history(index)
if history == self.input_buffer:
continue
if (as_prefix and history.startswith(substring)) \
or (not as_prefix and substring in history):
replace = True
break
if replace:
self._store_edits()
self._history_index = index
self.input_buffer = history
return replace
def history_next(self, substring='', as_prefix=True):
""" If possible, set the input buffer to a subsequent history item.
Parameters
----------
substring : str, optional
If specified, search for an item with this substring.
as_prefix : bool, optional
If True, the substring must match at the beginning (default).
Returns
-------
Whether the input buffer was changed.
"""
index = self._history_index
replace = False
while index < len(self._history):
index += 1
history = self._get_edited_history(index)
if history == self.input_buffer:
continue
if (as_prefix and history.startswith(substring)) \
or (not as_prefix and substring in history):
replace = True
break
if replace:
self._store_edits()
self._history_index = index
self.input_buffer = history
return replace
def history_tail(self, n=10):
""" Get the local history list.
Parameters
----------
n : int
The (maximum) number of history items to get.
"""
return self._history[-n:]
#---------------------------------------------------------------------------
# 'HistoryConsoleWidget' protected interface
#---------------------------------------------------------------------------
def _history_locked(self):
""" Returns whether history movement is locked.
"""
return (self.history_lock and
(self._get_edited_history(self._history_index) !=
self.input_buffer) and
(self._get_prompt_cursor().blockNumber() !=
self._get_end_cursor().blockNumber()))
def _get_edited_history(self, index):
""" Retrieves a history item, possibly with temporary edits.
"""
if index in self._history_edits:
return self._history_edits[index]
elif index == len(self._history):
return unicode_type()
return self._history[index]
def _set_history(self, history):
""" Replace the current history with a sequence of history items.
"""
self._history = list(history)
self._history_edits = {}
self._history_index = len(self._history)
def _store_edits(self):
""" If there are edits to the current input buffer, store them.
"""
current = self.input_buffer
if self._history_index == len(self._history) or \
self._history[self._history_index] != current:
self._history_edits[self._history_index] = current

View file

@ -0,0 +1,90 @@
""" Defines an in-process KernelManager with signals and slots.
"""
from qtpy import QtCore
from ipykernel.inprocess import (
InProcessHBChannel, InProcessKernelClient, InProcessKernelManager,
)
from ipykernel.inprocess.channels import InProcessChannel
from traitlets import Type
from .util import SuperQObject
from .kernel_mixins import (
QtKernelClientMixin, QtKernelManagerMixin,
)
from .rich_jupyter_widget import RichJupyterWidget
class QtInProcessChannel(SuperQObject, InProcessChannel):
# Emitted when the channel is started.
started = QtCore.Signal()
# Emitted when the channel is stopped.
stopped = QtCore.Signal()
# Emitted when any message is received.
message_received = QtCore.Signal(object)
def start(self):
""" Reimplemented to emit signal.
"""
super(QtInProcessChannel, self).start()
self.started.emit()
def stop(self):
""" Reimplemented to emit signal.
"""
super(QtInProcessChannel, self).stop()
self.stopped.emit()
def call_handlers_later(self, *args, **kwds):
""" Call the message handlers later.
"""
do_later = lambda: self.call_handlers(*args, **kwds)
QtCore.QTimer.singleShot(0, do_later)
def call_handlers(self, msg):
self.message_received.emit(msg)
def process_events(self):
""" Process any pending GUI events.
"""
QtCore.QCoreApplication.instance().processEvents()
def flush(self, timeout=1.0):
""" Reimplemented to ensure that signals are dispatched immediately.
"""
super(QtInProcessChannel, self).flush()
self.process_events()
class QtInProcessHBChannel(SuperQObject, InProcessHBChannel):
# This signal will never be fired, but it needs to exist
kernel_died = QtCore.Signal()
class QtInProcessKernelClient(QtKernelClientMixin, InProcessKernelClient):
""" An in-process KernelManager with signals and slots.
"""
iopub_channel_class = Type(QtInProcessChannel)
shell_channel_class = Type(QtInProcessChannel)
stdin_channel_class = Type(QtInProcessChannel)
hb_channel_class = Type(QtInProcessHBChannel)
class QtInProcessKernelManager(QtKernelManagerMixin, InProcessKernelManager):
client_class = __module__ + '.QtInProcessKernelClient'
class QtInProcessRichJupyterWidget(RichJupyterWidget):
""" An in-process Jupyter Widget that enables multiline editing
"""
def _is_complete(self, source, interactive=True):
shell = self.kernel_manager.kernel.shell
status, indent_spaces = \
shell.input_transformer_manager.check_complete(source)
if indent_spaces is None:
indent = ''
else:
indent = ' ' * indent_spaces
return status != 'incomplete', indent

View file

@ -0,0 +1,4 @@
import warnings
warnings.warn("qtconsole.ipython_widget is deprecated; "
"use qtconsole.jupyter_widget", DeprecationWarning)
from .jupyter_widget import *

View file

@ -0,0 +1,615 @@
"""A FrontendWidget that emulates a repl for a Jupyter kernel.
This supports the additional functionality provided by Jupyter kernel.
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from collections import namedtuple
import os.path
import re
from subprocess import Popen
import sys
import time
from textwrap import dedent
from warnings import warn
from qtpy import QtCore, QtGui
from IPython.lib.lexers import IPythonLexer, IPython3Lexer
from pygments.lexers import get_lexer_by_name
from pygments.util import ClassNotFound
from qtconsole import __version__
from traitlets import Bool, Unicode, observe, default
from .frontend_widget import FrontendWidget
from . import styles
#-----------------------------------------------------------------------------
# Constants
#-----------------------------------------------------------------------------
# Default strings to build and display input and output prompts (and separators
# in between)
default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
default_input_sep = '\n'
default_output_sep = ''
default_output_sep2 = ''
# Base path for most payload sources.
zmq_shell_source = 'ipykernel.zmqshell.ZMQInteractiveShell'
if sys.platform.startswith('win'):
default_editor = 'notepad'
else:
default_editor = ''
#-----------------------------------------------------------------------------
# JupyterWidget class
#-----------------------------------------------------------------------------
class IPythonWidget(FrontendWidget):
"""Dummy class for config inheritance. Destroyed below."""
class JupyterWidget(IPythonWidget):
"""A FrontendWidget for a Jupyter kernel."""
# If set, the 'custom_edit_requested(str, int)' signal will be emitted when
# an editor is needed for a file. This overrides 'editor' and 'editor_line'
# settings.
custom_edit = Bool(False)
custom_edit_requested = QtCore.Signal(object, object)
editor = Unicode(default_editor, config=True,
help="""
A command for invoking a GUI text editor. If the string contains a
{filename} format specifier, it will be used. Otherwise, the filename
will be appended to the end the command. To use a terminal text editor,
the command should launch a new terminal, e.g.
``"gnome-terminal -- vim"``.
""")
editor_line = Unicode(config=True,
help="""
The editor command to use when a specific line number is requested. The
string should contain two format specifiers: {line} and {filename}. If
this parameter is not specified, the line number option to the %edit
magic will be ignored.
""")
style_sheet = Unicode(config=True,
help="""
A CSS stylesheet. The stylesheet can contain classes for:
1. Qt: QPlainTextEdit, QFrame, QWidget, etc
2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
3. QtConsole: .error, .in-prompt, .out-prompt, etc
""")
syntax_style = Unicode(config=True,
help="""
If not empty, use this Pygments style for syntax highlighting.
Otherwise, the style sheet is queried for Pygments style
information.
""")
# Prompts.
in_prompt = Unicode(default_in_prompt, config=True)
out_prompt = Unicode(default_out_prompt, config=True)
input_sep = Unicode(default_input_sep, config=True)
output_sep = Unicode(default_output_sep, config=True)
output_sep2 = Unicode(default_output_sep2, config=True)
# JupyterWidget protected class variables.
_PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
_payload_source_edit = 'edit_magic'
_payload_source_exit = 'ask_exit'
_payload_source_next_input = 'set_next_input'
_payload_source_page = 'page'
_retrying_history_request = False
_starting = False
#---------------------------------------------------------------------------
# 'object' interface
#---------------------------------------------------------------------------
def __init__(self, *args, **kw):
super(JupyterWidget, self).__init__(*args, **kw)
# JupyterWidget protected variables.
self._payload_handlers = {
self._payload_source_edit : self._handle_payload_edit,
self._payload_source_exit : self._handle_payload_exit,
self._payload_source_page : self._handle_payload_page,
self._payload_source_next_input : self._handle_payload_next_input }
self._previous_prompt_obj = None
self._keep_kernel_on_exit = None
# Initialize widget styling.
if self.style_sheet:
self._style_sheet_changed()
self._syntax_style_changed()
else:
self.set_default_style()
# Initialize language name.
self.language_name = None
#---------------------------------------------------------------------------
# 'BaseFrontendMixin' abstract interface
#
# For JupyterWidget, override FrontendWidget methods which implement the
# BaseFrontend Mixin abstract interface
#---------------------------------------------------------------------------
def _handle_complete_reply(self, rep):
"""Support Jupyter's improved completion machinery.
"""
self.log.debug("complete: %s", rep.get('content', ''))
cursor = self._get_cursor()
info = self._request_info.get('complete')
if (info and info.id == rep['parent_header']['msg_id']
and info.pos == self._get_input_buffer_cursor_pos()
and info.code == self.input_buffer):
content = rep['content']
matches = content['matches']
start = content['cursor_start']
end = content['cursor_end']
start = max(start, 0)
end = max(end, start)
# Move the control's cursor to the desired end point
cursor_pos = self._get_input_buffer_cursor_pos()
if end < cursor_pos:
cursor.movePosition(QtGui.QTextCursor.Left,
n=(cursor_pos - end))
elif end > cursor_pos:
cursor.movePosition(QtGui.QTextCursor.Right,
n=(end - cursor_pos))
# This line actually applies the move to control's cursor
self._control.setTextCursor(cursor)
offset = end - start
# Move the local cursor object to the start of the match and
# complete.
cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
self._complete_with_items(cursor, matches)
def _handle_execute_reply(self, msg):
"""Support prompt requests.
"""
msg_id = msg['parent_header'].get('msg_id')
info = self._request_info['execute'].get(msg_id)
if info and info.kind == 'prompt':
content = msg['content']
if content['status'] == 'aborted':
self._show_interpreter_prompt()
else:
number = content['execution_count'] + 1
self._show_interpreter_prompt(number)
self._request_info['execute'].pop(msg_id)
else:
super(JupyterWidget, self)._handle_execute_reply(msg)
def _handle_history_reply(self, msg):
""" Handle history tail replies, which are only supported
by Jupyter kernels.
"""
content = msg['content']
if 'history' not in content:
self.log.error("History request failed: %r"%content)
if content.get('status', '') == 'aborted' and \
not self._retrying_history_request:
# a *different* action caused this request to be aborted, so
# we should try again.
self.log.error("Retrying aborted history request")
# prevent multiple retries of aborted requests:
self._retrying_history_request = True
# wait out the kernel's queue flush, which is currently timed at 0.1s
time.sleep(0.25)
self.kernel_client.history(hist_access_type='tail',n=1000)
else:
self._retrying_history_request = False
return
# reset retry flag
self._retrying_history_request = False
history_items = content['history']
self.log.debug("Received history reply with %i entries", len(history_items))
items = []
last_cell = u""
for _, _, cell in history_items:
cell = cell.rstrip()
if cell != last_cell:
items.append(cell)
last_cell = cell
self._set_history(items)
def _insert_other_input(self, cursor, content, remote=True):
"""Insert function for input from other frontends"""
n = content.get('execution_count', 0)
prompt = self._make_in_prompt(n, remote=remote)
cont_prompt = self._make_continuation_prompt(self._prompt, remote=remote)
cursor.insertText('\n')
for i, line in enumerate(content['code'].strip().split('\n')):
if i == 0:
self._insert_html(cursor, prompt)
else:
self._insert_html(cursor, cont_prompt)
self._insert_plain_text(cursor, line + '\n')
# Update current prompt number
self._update_prompt(n + 1)
def _handle_execute_input(self, msg):
"""Handle an execute_input message"""
self.log.debug("execute_input: %s", msg.get('content', ''))
if self.include_output(msg):
self._append_custom(
self._insert_other_input, msg['content'], before_prompt=True)
elif not self._prompt:
self._append_custom(
self._insert_other_input, msg['content'],
before_prompt=True, remote=False)
def _handle_execute_result(self, msg):
"""Handle an execute_result message"""
self.log.debug("execute_result: %s", msg.get('content', ''))
if self.include_output(msg):
self.flush_clearoutput()
content = msg['content']
prompt_number = content.get('execution_count', 0)
data = content['data']
if 'text/plain' in data:
self._append_plain_text(self.output_sep, before_prompt=True)
self._append_html(
self._make_out_prompt(prompt_number, remote=not self.from_here(msg)),
before_prompt=True
)
text = data['text/plain']
# If the repr is multiline, make sure we start on a new line,
# so that its lines are aligned.
if "\n" in text and not self.output_sep.endswith("\n"):
self._append_plain_text('\n', before_prompt=True)
self._append_plain_text(text + self.output_sep2, before_prompt=True)
if not self.from_here(msg):
self._append_plain_text('\n', before_prompt=True)
def _handle_display_data(self, msg):
"""The base handler for the ``display_data`` message."""
# For now, we don't display data from other frontends, but we
# eventually will as this allows all frontends to monitor the display
# data. But we need to figure out how to handle this in the GUI.
if self.include_output(msg):
self.flush_clearoutput()
data = msg['content']['data']
metadata = msg['content']['metadata']
# In the regular JupyterWidget, we simply print the plain text
# representation.
if 'text/plain' in data:
text = data['text/plain']
self._append_plain_text(text, True)
# This newline seems to be needed for text and html output.
self._append_plain_text(u'\n', True)
def _handle_kernel_info_reply(self, rep):
"""Handle kernel info replies."""
content = rep['content']
self.language_name = content['language_info']['name']
pygments_lexer = content['language_info'].get('pygments_lexer', '')
try:
# Other kernels with pygments_lexer info will have to be
# added here by hand.
if pygments_lexer == 'ipython3':
lexer = IPython3Lexer()
elif pygments_lexer == 'ipython2':
lexer = IPythonLexer()
else:
lexer = get_lexer_by_name(self.language_name)
self._highlighter._lexer = lexer
except ClassNotFound:
pass
self.kernel_banner = content.get('banner', '')
if self._starting:
# finish handling started channels
self._starting = False
super(JupyterWidget, self)._started_channels()
def _started_channels(self):
"""Make a history request"""
self._starting = True
self.kernel_client.kernel_info()
self.kernel_client.history(hist_access_type='tail', n=1000)
#---------------------------------------------------------------------------
# 'FrontendWidget' protected interface
#---------------------------------------------------------------------------
def _process_execute_error(self, msg):
"""Handle an execute_error message"""
self.log.debug("execute_error: %s", msg.get('content', ''))
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, before_prompt=not self.from_here(msg))
def _process_execute_payload(self, item):
""" Reimplemented to dispatch payloads to handler methods.
"""
handler = self._payload_handlers.get(item['source'])
if handler is None:
# We have no handler for this type of payload, simply ignore it
return False
else:
handler(item)
return True
def _show_interpreter_prompt(self, number=None):
""" Reimplemented for IPython-style prompts.
"""
# If a number was not specified, make a prompt number request.
if number is None:
msg_id = self.kernel_client.execute('', silent=True)
info = self._ExecutionRequest(msg_id, 'prompt')
self._request_info['execute'][msg_id] = info
return
# 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._prompt_sep = self.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 = self._PromptBlock(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 _update_prompt(self, new_prompt_number):
"""Replace the last displayed prompt with a new one."""
if self._previous_prompt_obj is None:
return
block = self._previous_prompt_obj.block
# Make sure the prompt block has not been erased.
if block.isValid() and block.text():
# 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(new_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())
# Update the prompt cursor
self._prompt_cursor.setPosition(cursor.position() - 1)
# Store the updated prompt.
block = self._control.document().lastBlock()
length = len(self._prompt)
self._previous_prompt_obj = self._PromptBlock(block, length, new_prompt_number)
def _show_interpreter_prompt_for_reply(self, msg):
""" Reimplemented for IPython-style prompts.
"""
# Update the old prompt number if necessary.
content = msg['content']
# abort replies do not have any keys:
if content['status'] == 'aborted':
if self._previous_prompt_obj:
previous_prompt_number = self._previous_prompt_obj.number
else:
previous_prompt_number = 0
else:
previous_prompt_number = content['execution_count']
if self._previous_prompt_obj and \
self._previous_prompt_obj.number != previous_prompt_number:
self._update_prompt(previous_prompt_number)
self._previous_prompt_obj = None
# Show a new prompt with the kernel's estimated prompt number.
self._show_interpreter_prompt(previous_prompt_number + 1)
#---------------------------------------------------------------------------
# 'JupyterWidget' interface
#---------------------------------------------------------------------------
def set_default_style(self, colors='lightbg'):
""" Sets the widget style to the class defaults.
Parameters
----------
colors : str, optional (default lightbg)
Whether to use the default light background or dark
background or B&W style.
"""
colors = colors.lower()
if colors=='lightbg':
self.style_sheet = styles.default_light_style_sheet
self.syntax_style = styles.default_light_syntax_style
elif colors=='linux':
self.style_sheet = styles.default_dark_style_sheet
self.syntax_style = styles.default_dark_syntax_style
elif colors=='nocolor':
self.style_sheet = styles.default_bw_style_sheet
self.syntax_style = styles.default_bw_syntax_style
else:
raise KeyError("No such color scheme: %s"%colors)
#---------------------------------------------------------------------------
# 'JupyterWidget' protected 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.
"""
if self.custom_edit:
self.custom_edit_requested.emit(filename, line)
elif not self.editor:
self._append_plain_text('No default editor available.\n'
'Specify a GUI text editor in the `JupyterWidget.editor` '
'configurable to enable the %edit magic')
else:
try:
filename = '"%s"' % filename
if line and self.editor_line:
command = self.editor_line.format(filename=filename,
line=line)
else:
try:
command = self.editor.format()
except KeyError:
command = self.editor.format(filename=filename)
else:
command += ' ' + filename
except KeyError:
self._append_plain_text('Invalid editor command.\n')
else:
try:
Popen(command, shell=True)
except OSError:
msg = 'Opening editor with command "%s" failed.\n'
self._append_plain_text(msg % command)
def _make_in_prompt(self, number, remote=False):
""" Given a prompt number, returns an HTML In prompt.
"""
try:
body = self.in_prompt % number
except TypeError:
# allow in_prompt to leave out number, e.g. '>>> '
from xml.sax.saxutils import escape
body = escape(self.in_prompt)
if remote:
body = self.other_output_prefix + body
return '<span class="in-prompt">%s</span>' % body
def _make_continuation_prompt(self, prompt, remote=False):
""" 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)
if remote:
space_count += len(self.other_output_prefix.rsplit('\n')[-1])
body = '&nbsp;' * space_count + end_chars
return '<span class="in-prompt">%s</span>' % body
def _make_out_prompt(self, number, remote=False):
""" Given a prompt number, returns an HTML Out prompt.
"""
try:
body = self.out_prompt % number
except TypeError:
# allow out_prompt to leave out number, e.g. '<<< '
from xml.sax.saxutils import escape
body = escape(self.out_prompt)
if remote:
body = self.other_output_prefix + body
return '<span class="out-prompt">%s</span>' % body
#------ Payload handlers --------------------------------------------------
# Payload handlers with a generic interface: each takes the opaque payload
# dict, unpacks it and calls the underlying functions with the necessary
# arguments.
def _handle_payload_edit(self, item):
self._edit(item['filename'], item['line_number'])
def _handle_payload_exit(self, item):
self._keep_kernel_on_exit = item['keepkernel']
self.exit_requested.emit(self)
def _handle_payload_next_input(self, item):
self.input_buffer = item['text']
def _handle_payload_page(self, item):
# Since the plain text widget supports only a very small subset of HTML
# and we have no control over the HTML source, we only page HTML
# payloads in the rich text widget.
data = item['data']
if 'text/html' in data and self.kind == 'rich':
self._page(data['text/html'], html=True)
else:
self._page(data['text/plain'], html=False)
#------ Trait change handlers --------------------------------------------
@observe('style_sheet')
def _style_sheet_changed(self, changed=None):
""" Set the style sheets of the underlying widgets.
"""
self.setStyleSheet(self.style_sheet)
if self._control is not None:
self._control.document().setDefaultStyleSheet(self.style_sheet)
if self._page_control is not None:
self._page_control.document().setDefaultStyleSheet(self.style_sheet)
@observe('syntax_style')
def _syntax_style_changed(self, changed=None):
""" Set the style for the syntax highlighter.
"""
if self._highlighter is None:
# ignore premature calls
return
if self.syntax_style:
self._highlighter.set_style(self.syntax_style)
self._ansi_processor.set_background_color(self.syntax_style)
else:
self._highlighter.set_style_sheet(self.style_sheet)
#------ Trait default initializers -----------------------------------------
@default('banner')
def _banner_default(self):
return "Jupyter QtConsole {version}\n".format(version=__version__)
# Clobber IPythonWidget above:
class IPythonWidget(JupyterWidget):
"""Deprecated class; use JupyterWidget."""
def __init__(self, *a, **kw):
warn("IPythonWidget is deprecated; use JupyterWidget",
DeprecationWarning)
super(IPythonWidget, self).__init__(*a, **kw)

View file

@ -0,0 +1,56 @@
"""Defines a KernelManager that provides signals and slots."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from qtpy import QtCore
from traitlets import HasTraits, Type
from .util import MetaQObjectHasTraits, SuperQObject
from .comms import CommManager
class QtKernelRestarterMixin(MetaQObjectHasTraits('NewBase', (HasTraits, SuperQObject), {})):
_timer = None
class QtKernelManagerMixin(MetaQObjectHasTraits('NewBase', (HasTraits, SuperQObject), {})):
""" A KernelClient that provides signals and slots.
"""
kernel_restarted = QtCore.Signal()
class QtKernelClientMixin(MetaQObjectHasTraits('NewBase', (HasTraits, SuperQObject), {})):
""" A KernelClient that provides signals and slots.
"""
# Emitted when the kernel client has started listening.
started_channels = QtCore.Signal()
# Emitted when the kernel client has stopped listening.
stopped_channels = QtCore.Signal()
#---------------------------------------------------------------------------
# 'KernelClient' interface
#---------------------------------------------------------------------------
def __init__(self, *args, **kwargs):
super(QtKernelClientMixin, self).__init__(*args, **kwargs)
self.comm_manager = None
#------ Channel management -------------------------------------------------
def start_channels(self, *args, **kw):
""" Reimplemented to emit signal.
"""
super(QtKernelClientMixin, self).start_channels(*args, **kw)
self.started_channels.emit()
self.comm_manager = CommManager(parent=self, kernel_client=self)
def stop_channels(self):
""" Reimplemented to emit signal.
"""
super(QtKernelClientMixin, self).stop_channels()
self.stopped_channels.emit()
self.comm_manager = None

View file

@ -0,0 +1,128 @@
""" A generic Emacs-style kill ring, as well as a Qt-specific version.
"""
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
# System library imports
from qtpy import QtCore, QtWidgets, QtGui
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
class KillRing(object):
""" A generic Emacs-style kill ring.
"""
def __init__(self):
self.clear()
def clear(self):
""" Clears the kill ring.
"""
self._index = -1
self._ring = []
def kill(self, text):
""" Adds some killed text to the ring.
"""
self._ring.append(text)
def yank(self):
""" Yank back the most recently killed text.
Returns
-------
A text string or None.
"""
self._index = len(self._ring)
return self.rotate()
def rotate(self):
""" Rotate the kill ring, then yank back the new top.
Returns
-------
A text string or None.
"""
self._index -= 1
if self._index >= 0:
return self._ring[self._index]
return None
class QtKillRing(QtCore.QObject):
""" A kill ring attached to Q[Plain]TextEdit.
"""
#--------------------------------------------------------------------------
# QtKillRing interface
#--------------------------------------------------------------------------
def __init__(self, text_edit):
""" Create a kill ring attached to the specified Qt text edit.
"""
assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
super(QtKillRing, self).__init__()
self._ring = KillRing()
self._prev_yank = None
self._skip_cursor = False
self._text_edit = text_edit
text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
def clear(self):
""" Clears the kill ring.
"""
self._ring.clear()
self._prev_yank = None
def kill(self, text):
""" Adds some killed text to the ring.
"""
self._ring.kill(text)
def kill_cursor(self, cursor):
""" Kills the text selected by the give cursor.
"""
text = cursor.selectedText()
if text:
cursor.removeSelectedText()
self.kill(text)
def yank(self):
""" Yank back the most recently killed text.
"""
text = self._ring.yank()
if text:
self._skip_cursor = True
cursor = self._text_edit.textCursor()
cursor.insertText(text)
self._prev_yank = text
def rotate(self):
""" Rotate the kill ring, then yank back the new top.
"""
if self._prev_yank:
text = self._ring.rotate()
if text:
self._skip_cursor = True
cursor = self._text_edit.textCursor()
cursor.movePosition(QtGui.QTextCursor.Left,
QtGui.QTextCursor.KeepAnchor,
n = len(self._prev_yank))
cursor.insertText(text)
self._prev_yank = text
#--------------------------------------------------------------------------
# Protected interface
#--------------------------------------------------------------------------
#------ Signal handlers ----------------------------------------------------
def _cursor_position_changed(self):
if self._skip_cursor:
self._skip_cursor = False
else:
self._prev_yank = None

View file

@ -0,0 +1,884 @@
"""The Qt MainWindow for the QtConsole
This is a tabbed pseudo-terminal of Jupyter sessions, with a menu bar for
common actions.
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import sys
import webbrowser
from threading import Thread
from jupyter_core.paths import jupyter_runtime_dir
from pygments.styles import get_all_styles
from qtpy import QtGui, QtCore, QtWidgets
from qtconsole import styles
from qtconsole.jupyter_widget import JupyterWidget
from qtconsole.usage import gui_reference
def background(f):
"""call a function in a simple thread, to prevent blocking"""
t = Thread(target=f)
t.start()
return t
class MainWindow(QtWidgets.QMainWindow):
#---------------------------------------------------------------------------
# 'object' interface
#---------------------------------------------------------------------------
def __init__(self, app,
confirm_exit=True,
new_frontend_factory=None, slave_frontend_factory=None,
connection_frontend_factory=None,
):
""" Create a tabbed MainWindow for managing FrontendWidgets
Parameters
----------
app : reference to QApplication parent
confirm_exit : bool, optional
Whether we should prompt on close of tabs
new_frontend_factory : callable
A callable that returns a new JupyterWidget instance, attached to
its own running kernel.
slave_frontend_factory : callable
A callable that takes an existing JupyterWidget, and returns a new
JupyterWidget instance, attached to the same kernel.
"""
super(MainWindow, self).__init__()
self._kernel_counter = 0
self._external_kernel_counter = 0
self._app = app
self.confirm_exit = confirm_exit
self.new_frontend_factory = new_frontend_factory
self.slave_frontend_factory = slave_frontend_factory
self.connection_frontend_factory = connection_frontend_factory
self.tab_widget = QtWidgets.QTabWidget(self)
self.tab_widget.setDocumentMode(True)
self.tab_widget.setTabsClosable(True)
self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
self.setCentralWidget(self.tab_widget)
# hide tab bar at first, since we have no tabs:
self.tab_widget.tabBar().setVisible(False)
# prevent focus in tab bar
self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
def update_tab_bar_visibility(self):
""" update visibility of the tabBar depending of the number of tab
0 or 1 tab, tabBar hidden
2+ tabs, tabBar visible
send a self.close if number of tab ==0
need to be called explicitly, or be connected to tabInserted/tabRemoved
"""
if self.tab_widget.count() <= 1:
self.tab_widget.tabBar().setVisible(False)
else:
self.tab_widget.tabBar().setVisible(True)
if self.tab_widget.count()==0 :
self.close()
@property
def next_kernel_id(self):
"""constantly increasing counter for kernel IDs"""
c = self._kernel_counter
self._kernel_counter += 1
return c
@property
def next_external_kernel_id(self):
"""constantly increasing counter for external kernel IDs"""
c = self._external_kernel_counter
self._external_kernel_counter += 1
return c
@property
def active_frontend(self):
return self.tab_widget.currentWidget()
def create_tab_with_new_frontend(self):
"""create a new frontend and attach it to a new tab"""
widget = self.new_frontend_factory()
self.add_tab_with_frontend(widget)
def set_window_title(self):
"""Set the title of the console window"""
old_title = self.windowTitle()
title, ok = QtWidgets.QInputDialog.getText(self,
"Rename Window",
"New title:",
text=old_title)
if ok:
self.setWindowTitle(title)
def create_tab_with_existing_kernel(self):
"""create a new frontend attached to an external kernel in a new tab"""
connection_file, file_type = QtWidgets.QFileDialog.getOpenFileName(self,
"Connect to Existing Kernel",
jupyter_runtime_dir(),
"Connection file (*.json)")
if not connection_file:
return
widget = self.connection_frontend_factory(connection_file)
name = "external {}".format(self.next_external_kernel_id)
self.add_tab_with_frontend(widget, name=name)
def create_tab_with_current_kernel(self):
"""create a new frontend attached to the same kernel as the current tab"""
current_widget = self.tab_widget.currentWidget()
current_widget_index = self.tab_widget.indexOf(current_widget)
current_widget_name = self.tab_widget.tabText(current_widget_index)
widget = self.slave_frontend_factory(current_widget)
if 'slave' in current_widget_name:
# don't keep stacking slaves
name = current_widget_name
else:
name = '(%s) slave' % current_widget_name
self.add_tab_with_frontend(widget,name=name)
def set_tab_title(self):
"""Set the title of the current tab"""
old_title = self.tab_widget.tabText(self.tab_widget.currentIndex())
title, ok = QtWidgets.QInputDialog.getText(self,
"Rename Tab",
"New title:",
text=old_title)
if ok:
self.tab_widget.setTabText(self.tab_widget.currentIndex(), title)
def close_tab(self,current_tab):
""" Called when you need to try to close a tab.
It takes the number of the tab to be closed as argument, or a reference
to the widget inside this tab
"""
# let's be sure "tab" and "closing widget" are respectively the index
# of the tab to close and a reference to the frontend to close
if type(current_tab) is not int :
current_tab = self.tab_widget.indexOf(current_tab)
closing_widget=self.tab_widget.widget(current_tab)
# when trying to be closed, widget might re-send a request to be
# closed again, but will be deleted when event will be processed. So
# need to check that widget still exists and skip if not. One example
# of this is when 'exit' is sent in a slave tab. 'exit' will be
# re-sent by this function on the master widget, which ask all slave
# widgets to exit
if closing_widget is None:
return
#get a list of all slave widgets on the same kernel.
slave_tabs = self.find_slave_widgets(closing_widget)
keepkernel = None #Use the prompt by default
if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
keepkernel = closing_widget._keep_kernel_on_exit
# If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
# we set local slave tabs._hidden to True to avoid prompting for kernel
# restart when they get the signal. and then "forward" the 'exit'
# to the main window
if keepkernel is not None:
for tab in slave_tabs:
tab._hidden = True
if closing_widget in slave_tabs:
try :
self.find_master_tab(closing_widget).execute('exit')
except AttributeError:
self.log.info("Master already closed or not local, closing only current tab")
self.tab_widget.removeTab(current_tab)
self.update_tab_bar_visibility()
return
kernel_client = closing_widget.kernel_client
kernel_manager = closing_widget.kernel_manager
if keepkernel is None and not closing_widget._confirm_exit:
# don't prompt, just terminate the kernel if we own it
# or leave it alone if we don't
keepkernel = closing_widget._existing
if keepkernel is None: #show prompt
if kernel_client and kernel_client.channels_running:
title = self.window().windowTitle()
cancel = QtWidgets.QMessageBox.Cancel
okay = QtWidgets.QMessageBox.Ok
if closing_widget._may_close:
msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
info = "Would you like to quit the Kernel and close all attached Consoles as well?"
justthis = QtWidgets.QPushButton("&No, just this Tab", self)
justthis.setShortcut('N')
closeall = QtWidgets.QPushButton("&Yes, close all", self)
closeall.setShortcut('Y')
# allow ctrl-d ctrl-d exit, like in terminal
closeall.setShortcut('Ctrl+D')
box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question,
title, msg)
box.setInformativeText(info)
box.addButton(cancel)
box.addButton(justthis, QtWidgets.QMessageBox.NoRole)
box.addButton(closeall, QtWidgets.QMessageBox.YesRole)
box.setDefaultButton(closeall)
box.setEscapeButton(cancel)
pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
box.setIconPixmap(pixmap)
reply = box.exec_()
if reply == 1: # close All
for slave in slave_tabs:
background(slave.kernel_client.stop_channels)
self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
kernel_manager.shutdown_kernel()
self.tab_widget.removeTab(current_tab)
background(kernel_client.stop_channels)
elif reply == 0: # close Console
if not closing_widget._existing:
# Have kernel: don't quit, just close the tab
closing_widget.execute("exit True")
self.tab_widget.removeTab(current_tab)
background(kernel_client.stop_channels)
else:
reply = QtWidgets.QMessageBox.question(self, title,
"Are you sure you want to close this Console?"+
"\nThe Kernel and other Consoles will remain active.",
okay|cancel,
defaultButton=okay
)
if reply == okay:
self.tab_widget.removeTab(current_tab)
elif keepkernel: #close console but leave kernel running (no prompt)
self.tab_widget.removeTab(current_tab)
background(kernel_client.stop_channels)
else: #close console and kernel (no prompt)
self.tab_widget.removeTab(current_tab)
if kernel_client and kernel_client.channels_running:
for slave in slave_tabs:
background(slave.kernel_client.stop_channels)
self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
if kernel_manager:
kernel_manager.shutdown_kernel()
background(kernel_client.stop_channels)
self.update_tab_bar_visibility()
def add_tab_with_frontend(self,frontend,name=None):
""" insert a tab with a given frontend in the tab bar, and give it a name
"""
if not name:
name = 'kernel %i' % self.next_kernel_id
self.tab_widget.addTab(frontend,name)
self.update_tab_bar_visibility()
self.make_frontend_visible(frontend)
frontend.exit_requested.connect(self.close_tab)
def next_tab(self):
self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
def prev_tab(self):
self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
def make_frontend_visible(self,frontend):
widget_index=self.tab_widget.indexOf(frontend)
if widget_index > 0 :
self.tab_widget.setCurrentIndex(widget_index)
def find_master_tab(self,tab,as_list=False):
"""
Try to return the frontend that owns the kernel attached to the given widget/tab.
Only finds frontend owned by the current application. Selection
based on port of the kernel might be inaccurate if several kernel
on different ip use same port number.
This function does the conversion tabNumber/widget if needed.
Might return None if no master widget (non local kernel)
Will crash if more than 1 masterWidget
When asList set to True, always return a list of widget(s) owning
the kernel. The list might be empty or containing several Widget.
"""
#convert from/to int/richIpythonWidget if needed
if isinstance(tab, int):
tab = self.tab_widget.widget(tab)
km=tab.kernel_client
#build list of all widgets
widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
# widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
# And should have a _may_close attribute
filtered_widget_list = [ widget for widget in widget_list if
widget.kernel_client.connection_file == km.connection_file and
hasattr(widget,'_may_close') ]
# the master widget is the one that may close the kernel
master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
if as_list:
return master_widget
assert(len(master_widget)<=1 )
if len(master_widget)==0:
return None
return master_widget[0]
def find_slave_widgets(self,tab):
"""return all the frontends that do not own the kernel attached to the given widget/tab.
Only find frontends owned by the current application. Selection
based on connection file of the kernel.
This function does the conversion tabNumber/widget if needed.
"""
#convert from/to int/richIpythonWidget if needed
if isinstance(tab, int):
tab = self.tab_widget.widget(tab)
km=tab.kernel_client
#build list of all widgets
widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
# widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
filtered_widget_list = ( widget for widget in widget_list if
widget.kernel_client.connection_file == km.connection_file)
# Get a list of all widget owning the same kernel and removed it from
# the previous cadidate. (better using sets ?)
master_widget_list = self.find_master_tab(tab, as_list=True)
slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
return slave_list
# Populate the menu bar with common actions and shortcuts
def add_menu_action(self, menu, action, defer_shortcut=False):
"""Add action to menu as well as self
So that when the menu bar is invisible, its actions are still available.
If defer_shortcut is True, set the shortcut context to widget-only,
where it will avoid conflict with shortcuts already bound to the
widgets themselves.
"""
menu.addAction(action)
self.addAction(action)
if defer_shortcut:
action.setShortcutContext(QtCore.Qt.WidgetShortcut)
def init_menu_bar(self):
#create menu in the order they should appear in the menu bar
self.init_file_menu()
self.init_edit_menu()
self.init_view_menu()
self.init_kernel_menu()
self.init_window_menu()
self.init_help_menu()
def init_file_menu(self):
self.file_menu = self.menuBar().addMenu("&File")
self.new_kernel_tab_act = QtWidgets.QAction("New Tab with &New kernel",
self,
shortcut="Ctrl+T",
triggered=self.create_tab_with_new_frontend)
self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
self.slave_kernel_tab_act = QtWidgets.QAction("New Tab with Sa&me kernel",
self,
shortcut="Ctrl+Shift+T",
triggered=self.create_tab_with_current_kernel)
self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
self.existing_kernel_tab_act = QtWidgets.QAction("New Tab with &Existing kernel",
self,
shortcut="Alt+T",
triggered=self.create_tab_with_existing_kernel)
self.add_menu_action(self.file_menu, self.existing_kernel_tab_act)
self.file_menu.addSeparator()
self.close_action=QtWidgets.QAction("&Close Tab",
self,
shortcut=QtGui.QKeySequence.Close,
triggered=self.close_active_frontend
)
self.add_menu_action(self.file_menu, self.close_action)
self.export_action=QtWidgets.QAction("&Save to HTML/XHTML",
self,
shortcut=QtGui.QKeySequence.Save,
triggered=self.export_action_active_frontend
)
self.add_menu_action(self.file_menu, self.export_action, True)
self.file_menu.addSeparator()
printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
# Only override the default if there is a collision.
# Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
printkey = "Ctrl+Shift+P"
self.print_action = QtWidgets.QAction("&Print",
self,
shortcut=printkey,
triggered=self.print_action_active_frontend)
self.add_menu_action(self.file_menu, self.print_action, True)
if sys.platform != 'darwin':
# OSX always has Quit in the Application menu, only add it
# to the File menu elsewhere.
self.file_menu.addSeparator()
self.quit_action = QtWidgets.QAction("&Quit",
self,
shortcut=QtGui.QKeySequence.Quit,
triggered=self.close,
)
self.add_menu_action(self.file_menu, self.quit_action)
def init_edit_menu(self):
self.edit_menu = self.menuBar().addMenu("&Edit")
self.undo_action = QtWidgets.QAction("&Undo",
self,
shortcut=QtGui.QKeySequence.Undo,
statusTip="Undo last action if possible",
triggered=self.undo_active_frontend
)
self.add_menu_action(self.edit_menu, self.undo_action)
self.redo_action = QtWidgets.QAction("&Redo",
self,
shortcut=QtGui.QKeySequence.Redo,
statusTip="Redo last action if possible",
triggered=self.redo_active_frontend)
self.add_menu_action(self.edit_menu, self.redo_action)
self.edit_menu.addSeparator()
self.cut_action = QtWidgets.QAction("&Cut",
self,
shortcut=QtGui.QKeySequence.Cut,
triggered=self.cut_active_frontend
)
self.add_menu_action(self.edit_menu, self.cut_action, True)
self.copy_action = QtWidgets.QAction("&Copy",
self,
shortcut=QtGui.QKeySequence.Copy,
triggered=self.copy_active_frontend
)
self.add_menu_action(self.edit_menu, self.copy_action, True)
self.copy_raw_action = QtWidgets.QAction("Copy (&Raw Text)",
self,
shortcut="Ctrl+Shift+C",
triggered=self.copy_raw_active_frontend
)
self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
self.paste_action = QtWidgets.QAction("&Paste",
self,
shortcut=QtGui.QKeySequence.Paste,
triggered=self.paste_active_frontend
)
self.add_menu_action(self.edit_menu, self.paste_action, True)
self.edit_menu.addSeparator()
selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
# Only override the default if there is a collision.
# Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
selectall = "Ctrl+Shift+A"
self.select_all_action = QtWidgets.QAction("Select Cell/&All",
self,
shortcut=selectall,
triggered=self.select_all_active_frontend
)
self.add_menu_action(self.edit_menu, self.select_all_action, True)
def init_view_menu(self):
self.view_menu = self.menuBar().addMenu("&View")
if sys.platform != 'darwin':
# disable on OSX, where there is always a menu bar
self.toggle_menu_bar_act = QtWidgets.QAction("Toggle &Menu Bar",
self,
shortcut="Ctrl+Shift+M",
statusTip="Toggle visibility of menubar",
triggered=self.toggle_menu_bar)
self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
self.full_screen_act = QtWidgets.QAction("&Full Screen",
self,
shortcut=fs_key,
statusTip="Toggle between Fullscreen and Normal Size",
triggered=self.toggleFullScreen)
self.add_menu_action(self.view_menu, self.full_screen_act)
self.view_menu.addSeparator()
self.increase_font_size = QtWidgets.QAction("Zoom &In",
self,
shortcut=QtGui.QKeySequence.ZoomIn,
triggered=self.increase_font_size_active_frontend
)
self.add_menu_action(self.view_menu, self.increase_font_size, True)
self.decrease_font_size = QtWidgets.QAction("Zoom &Out",
self,
shortcut=QtGui.QKeySequence.ZoomOut,
triggered=self.decrease_font_size_active_frontend
)
self.add_menu_action(self.view_menu, self.decrease_font_size, True)
self.reset_font_size = QtWidgets.QAction("Zoom &Reset",
self,
shortcut="Ctrl+0",
triggered=self.reset_font_size_active_frontend
)
self.add_menu_action(self.view_menu, self.reset_font_size, True)
self.view_menu.addSeparator()
self.clear_action = QtWidgets.QAction("&Clear Screen",
self,
shortcut='Ctrl+L',
statusTip="Clear the console",
triggered=self.clear_active_frontend)
self.add_menu_action(self.view_menu, self.clear_action)
self.pager_menu = self.view_menu.addMenu("&Pager")
hsplit_action = QtWidgets.QAction(".. &Horizontal Split",
self,
triggered=lambda: self.set_paging_active_frontend('hsplit'))
vsplit_action = QtWidgets.QAction(" : &Vertical Split",
self,
triggered=lambda: self.set_paging_active_frontend('vsplit'))
inside_action = QtWidgets.QAction(" &Inside Pager",
self,
triggered=lambda: self.set_paging_active_frontend('inside'))
self.pager_menu.addAction(hsplit_action)
self.pager_menu.addAction(vsplit_action)
self.pager_menu.addAction(inside_action)
available_syntax_styles = self.get_available_syntax_styles()
if len(available_syntax_styles) > 0:
self.syntax_style_menu = self.view_menu.addMenu("&Syntax Style")
style_group = QtWidgets.QActionGroup(self)
for style in available_syntax_styles:
action = QtWidgets.QAction("{}".format(style), self,
triggered=lambda v,
syntax_style=style:
self.set_syntax_style(
syntax_style=syntax_style))
action.setCheckable(True)
style_group.addAction(action)
self.syntax_style_menu.addAction(action)
if style == 'default':
action.setChecked(True)
self.syntax_style_menu.setDefaultAction(action)
def init_kernel_menu(self):
self.kernel_menu = self.menuBar().addMenu("&Kernel")
# Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
# keep the signal shortcuts to ctrl, rather than
# platform-default like we do elsewhere.
ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
self.interrupt_kernel_action = QtWidgets.QAction("&Interrupt current Kernel",
self,
triggered=self.interrupt_kernel_active_frontend,
shortcut=ctrl+"+C",
)
self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
self.restart_kernel_action = QtWidgets.QAction("&Restart current Kernel",
self,
triggered=self.restart_kernel_active_frontend,
shortcut=ctrl+"+.",
)
self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
self.kernel_menu.addSeparator()
self.confirm_restart_kernel_action = QtWidgets.QAction("&Confirm kernel restart",
self,
checkable=True,
checked=self.active_frontend.confirm_restart,
triggered=self.toggle_confirm_restart_active_frontend
)
self.add_menu_action(self.kernel_menu, self.confirm_restart_kernel_action)
self.tab_widget.currentChanged.connect(self.update_restart_checkbox)
def init_window_menu(self):
self.window_menu = self.menuBar().addMenu("&Window")
if sys.platform == 'darwin':
# add min/maximize actions to OSX, which lacks default bindings.
self.minimizeAct = QtWidgets.QAction("Mini&mize",
self,
shortcut="Ctrl+m",
statusTip="Minimize the window/Restore Normal Size",
triggered=self.toggleMinimized)
# maximize is called 'Zoom' on OSX for some reason
self.maximizeAct = QtWidgets.QAction("&Zoom",
self,
shortcut="Ctrl+Shift+M",
statusTip="Maximize the window/Restore Normal Size",
triggered=self.toggleMaximized)
self.add_menu_action(self.window_menu, self.minimizeAct)
self.add_menu_action(self.window_menu, self.maximizeAct)
self.window_menu.addSeparator()
prev_key = "Ctrl+Alt+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
self.prev_tab_act = QtWidgets.QAction("Pre&vious Tab",
self,
shortcut=prev_key,
statusTip="Select previous tab",
triggered=self.prev_tab)
self.add_menu_action(self.window_menu, self.prev_tab_act)
next_key = "Ctrl+Alt+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
self.next_tab_act = QtWidgets.QAction("Ne&xt Tab",
self,
shortcut=next_key,
statusTip="Select next tab",
triggered=self.next_tab)
self.add_menu_action(self.window_menu, self.next_tab_act)
self.rename_window_act = QtWidgets.QAction("Rename &Window",
self,
shortcut="Alt+R",
statusTip="Rename window",
triggered=self.set_window_title)
self.add_menu_action(self.window_menu, self.rename_window_act)
self.rename_current_tab_act = QtWidgets.QAction("&Rename Current Tab",
self,
shortcut="Ctrl+R",
statusTip="Rename current tab",
triggered=self.set_tab_title)
self.add_menu_action(self.window_menu, self.rename_current_tab_act)
def init_help_menu(self):
# please keep the Help menu in Mac Os even if empty. It will
# automatically contain a search field to search inside menus and
# please keep it spelled in English, as long as Qt Doesn't support
# a QAction.MenuRole like HelpMenuRole otherwise it will lose
# this search field functionality
self.help_menu = self.menuBar().addMenu("&Help")
# Help Menu
self.help_action = QtWidgets.QAction("Show &QtConsole help", self,
triggered=self._show_help)
self.online_help_action = QtWidgets.QAction("Open online &help", self,
triggered=self._open_online_help)
self.add_menu_action(self.help_menu, self.help_action)
self.add_menu_action(self.help_menu, self.online_help_action)
def _set_active_frontend_focus(self):
# this is a hack, self.active_frontend._control seems to be
# a private member. Unfortunately this is the only method
# to set focus reliably
QtCore.QTimer.singleShot(200, self.active_frontend._control.setFocus)
# minimize/maximize/fullscreen actions:
def toggle_menu_bar(self):
menu_bar = self.menuBar()
if menu_bar.isVisible():
menu_bar.setVisible(False)
else:
menu_bar.setVisible(True)
def toggleMinimized(self):
if not self.isMinimized():
self.showMinimized()
else:
self.showNormal()
def _show_help(self):
self.active_frontend._page(gui_reference)
def _open_online_help(self):
webbrowser.open("https://qtconsole.readthedocs.io", new=1, autoraise=True)
def toggleMaximized(self):
if not self.isMaximized():
self.showMaximized()
else:
self.showNormal()
# Min/Max imizing while in full screen give a bug
# when going out of full screen, at least on OSX
def toggleFullScreen(self):
if not self.isFullScreen():
self.showFullScreen()
if sys.platform == 'darwin':
self.maximizeAct.setEnabled(False)
self.minimizeAct.setEnabled(False)
else:
self.showNormal()
if sys.platform == 'darwin':
self.maximizeAct.setEnabled(True)
self.minimizeAct.setEnabled(True)
def set_paging_active_frontend(self, paging):
self.active_frontend._set_paging(paging)
def get_available_syntax_styles(self):
"""Get a list with the syntax styles available."""
styles = list(get_all_styles())
return sorted(styles)
def set_syntax_style(self, syntax_style):
"""Set up syntax style for the current console."""
if syntax_style=='bw':
colors='nocolor'
elif styles.dark_style(syntax_style):
colors='linux'
else:
colors='lightbg'
self.active_frontend.syntax_style = syntax_style
style_sheet = styles.sheet_from_template(syntax_style, colors)
self.active_frontend.style_sheet = style_sheet
self.active_frontend._syntax_style_changed()
self.active_frontend._style_sheet_changed()
self.active_frontend.reset(clear=True)
self.active_frontend._execute("%colors linux", True)
def close_active_frontend(self):
self.close_tab(self.active_frontend)
def restart_kernel_active_frontend(self):
self.active_frontend.request_restart_kernel()
def interrupt_kernel_active_frontend(self):
self.active_frontend.request_interrupt_kernel()
def toggle_confirm_restart_active_frontend(self):
widget = self.active_frontend
widget.confirm_restart = not widget.confirm_restart
self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
def update_restart_checkbox(self):
if self.active_frontend is None:
return
widget = self.active_frontend
self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
def clear_active_frontend(self):
self.active_frontend.clear()
def cut_active_frontend(self):
widget = self.active_frontend
if widget.can_cut():
widget.cut()
def copy_active_frontend(self):
widget = self.active_frontend
widget.copy()
def copy_raw_active_frontend(self):
self.active_frontend._copy_raw_action.trigger()
def paste_active_frontend(self):
widget = self.active_frontend
if widget.can_paste():
widget.paste()
def undo_active_frontend(self):
self.active_frontend.undo()
def redo_active_frontend(self):
self.active_frontend.redo()
def print_action_active_frontend(self):
self.active_frontend.print_action.trigger()
def export_action_active_frontend(self):
self.active_frontend.export_action.trigger()
def select_all_active_frontend(self):
self.active_frontend.select_all_action.trigger()
def increase_font_size_active_frontend(self):
self.active_frontend.increase_font_size.trigger()
def decrease_font_size_active_frontend(self):
self.active_frontend.decrease_font_size.trigger()
def reset_font_size_active_frontend(self):
self.active_frontend.reset_font_size.trigger()
#---------------------------------------------------------------------------
# QWidget interface
#---------------------------------------------------------------------------
def closeEvent(self, event):
""" Forward the close event to every tabs contained by the windows
"""
if self.tab_widget.count() == 0:
# no tabs, just close
event.accept()
return
# Do Not loop on the widget count as it change while closing
title = self.window().windowTitle()
cancel = QtWidgets.QMessageBox.Cancel
okay = QtWidgets.QMessageBox.Ok
accept_role = QtWidgets.QMessageBox.AcceptRole
if self.confirm_exit:
if self.tab_widget.count() > 1:
msg = "Close all tabs, stop all kernels, and Quit?"
else:
msg = "Close console, stop kernel, and Quit?"
info = "Kernels not started here (e.g. notebooks) will be left alone."
closeall = QtWidgets.QPushButton("&Quit", self)
closeall.setShortcut('Q')
box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question,
title, msg)
box.setInformativeText(info)
box.addButton(cancel)
box.addButton(closeall, QtWidgets.QMessageBox.YesRole)
box.setDefaultButton(closeall)
box.setEscapeButton(cancel)
pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
box.setIconPixmap(pixmap)
reply = box.exec_()
else:
reply = okay
if reply == cancel:
event.ignore()
return
if reply == okay or reply == accept_role:
while self.tab_widget.count() >= 1:
# prevent further confirmations:
widget = self.active_frontend
widget._confirm_exit = False
self.close_tab(widget)
event.accept()

View file

@ -0,0 +1,53 @@
""" Defines a KernelClient that provides signals and slots.
"""
from qtpy import QtCore
# Local imports
from traitlets import Bool, DottedObjectName
from jupyter_client import KernelManager
from jupyter_client.restarter import KernelRestarter
from .kernel_mixins import QtKernelManagerMixin, QtKernelRestarterMixin
class QtKernelRestarter(KernelRestarter, QtKernelRestarterMixin):
def start(self):
if self._timer is None:
self._timer = QtCore.QTimer()
self._timer.timeout.connect(self.poll)
self._timer.start(round(self.time_to_dead * 1000))
def stop(self):
self._timer.stop()
def poll(self):
super(QtKernelRestarter, self).poll()
class QtKernelManager(KernelManager, QtKernelManagerMixin):
"""A KernelManager with Qt signals for restart"""
client_class = DottedObjectName('qtconsole.client.QtKernelClient')
autorestart = Bool(True, config=True)
def start_restarter(self):
if self.autorestart and self.has_kernel:
if self._restarter is None:
self._restarter = QtKernelRestarter(
kernel_manager=self,
parent=self,
log=self.log,
)
self._restarter.add_callback(self._handle_kernel_restarted)
self._restarter.start()
def stop_restarter(self):
if self.autorestart:
if self._restarter is not None:
self._restarter.stop()
def _handle_kernel_restarted(self):
self.kernel_restarted.emit()

View file

@ -0,0 +1,242 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from qtpy import QtGui
from qtconsole.qstringhelpers import qstring_length
from ipython_genutils.py3compat import PY3, string_types
from pygments.formatters.html import HtmlFormatter
from pygments.lexer import RegexLexer, _TokenType, Text, Error
from pygments.lexers import PythonLexer, Python3Lexer
from pygments.styles import get_style_by_name
def get_tokens_unprocessed(self, text, stack=('root',)):
""" Split ``text`` into (tokentype, text) pairs.
Monkeypatched to store the final stack on the object itself.
The `text` parameter this gets passed is only the current line, so to
highlight things like multiline strings correctly, we need to retrieve
the state from the previous line (this is done in PygmentsHighlighter,
below), and use it to continue processing the current line.
"""
pos = 0
tokendefs = self._tokens
if hasattr(self, '_saved_state_stack'):
statestack = list(self._saved_state_stack)
else:
statestack = list(stack)
statetokens = tokendefs[statestack[-1]]
while 1:
for rexmatch, action, new_state in statetokens:
m = rexmatch(text, pos)
if m:
if action is not None:
if type(action) is _TokenType:
yield pos, action, m.group()
else:
for item in action(self, m):
yield item
pos = m.end()
if new_state is not None:
# state transition
if isinstance(new_state, tuple):
for state in new_state:
if state == '#pop':
statestack.pop()
elif state == '#push':
statestack.append(statestack[-1])
else:
statestack.append(state)
elif isinstance(new_state, int):
# pop
del statestack[new_state:]
elif new_state == '#push':
statestack.append(statestack[-1])
else:
assert False, "wrong state def: %r" % new_state
statetokens = tokendefs[statestack[-1]]
break
else:
try:
if text[pos] == '\n':
# at EOL, reset state to "root"
pos += 1
statestack = ['root']
statetokens = tokendefs['root']
yield pos, Text, u'\n'
continue
yield pos, Error, text[pos]
pos += 1
except IndexError:
break
self._saved_state_stack = list(statestack)
# Monkeypatch!
RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
class PygmentsBlockUserData(QtGui.QTextBlockUserData):
""" Storage for the user data associated with each line.
"""
syntax_stack = ('root',)
def __init__(self, **kwds):
for key, value in kwds.items():
setattr(self, key, value)
QtGui.QTextBlockUserData.__init__(self)
def __repr__(self):
attrs = ['syntax_stack']
kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
for attr in attrs ])
return 'PygmentsBlockUserData(%s)' % kwds
class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
""" Syntax highlighter that uses Pygments for parsing. """
#---------------------------------------------------------------------------
# 'QSyntaxHighlighter' interface
#---------------------------------------------------------------------------
def __init__(self, parent, lexer=None):
super(PygmentsHighlighter, self).__init__(parent)
self._document = self.document()
self._formatter = HtmlFormatter(nowrap=True)
self.set_style('default')
if lexer is not None:
self._lexer = lexer
else:
if PY3:
self._lexer = Python3Lexer()
else:
self._lexer = PythonLexer()
def highlightBlock(self, string):
""" Highlight a block of text.
"""
prev_data = self.currentBlock().previous().userData()
if prev_data is not None:
self._lexer._saved_state_stack = prev_data.syntax_stack
elif hasattr(self._lexer, '_saved_state_stack'):
del self._lexer._saved_state_stack
# Lex the text using Pygments
index = 0
for token, text in self._lexer.get_tokens(string):
length = qstring_length(text)
self.setFormat(index, length, self._get_format(token))
index += length
if hasattr(self._lexer, '_saved_state_stack'):
data = PygmentsBlockUserData(
syntax_stack=self._lexer._saved_state_stack)
self.currentBlock().setUserData(data)
# Clean up for the next go-round.
del self._lexer._saved_state_stack
#---------------------------------------------------------------------------
# 'PygmentsHighlighter' interface
#---------------------------------------------------------------------------
def set_style(self, style):
""" Sets the style to the specified Pygments style.
"""
if isinstance(style, string_types):
style = get_style_by_name(style)
self._style = style
self._clear_caches()
def set_style_sheet(self, stylesheet):
""" Sets a CSS stylesheet. The classes in the stylesheet should
correspond to those generated by:
pygmentize -S <style> -f html
Note that 'set_style' and 'set_style_sheet' completely override each
other, i.e. they cannot be used in conjunction.
"""
self._document.setDefaultStyleSheet(stylesheet)
self._style = None
self._clear_caches()
#---------------------------------------------------------------------------
# Protected interface
#---------------------------------------------------------------------------
def _clear_caches(self):
""" Clear caches for brushes and formats.
"""
self._brushes = {}
self._formats = {}
def _get_format(self, token):
""" Returns a QTextCharFormat for token or None.
"""
if token in self._formats:
return self._formats[token]
if self._style is None:
result = self._get_format_from_document(token, self._document)
else:
result = self._get_format_from_style(token, self._style)
self._formats[token] = result
return result
def _get_format_from_document(self, token, document):
""" Returns a QTextCharFormat for token by
"""
code, html = next(self._formatter._format_lines([(token, u'dummy')]))
self._document.setHtml(html)
return QtGui.QTextCursor(self._document).charFormat()
def _get_format_from_style(self, token, style):
""" Returns a QTextCharFormat for token by reading a Pygments style.
"""
result = QtGui.QTextCharFormat()
for key, value in style.style_for_token(token).items():
if value:
if key == 'color':
result.setForeground(self._get_brush(value))
elif key == 'bgcolor':
result.setBackground(self._get_brush(value))
elif key == 'bold':
result.setFontWeight(QtGui.QFont.Bold)
elif key == 'italic':
result.setFontItalic(True)
elif key == 'underline':
result.setUnderlineStyle(
QtGui.QTextCharFormat.SingleUnderline)
elif key == 'sans':
result.setFontStyleHint(QtGui.QFont.SansSerif)
elif key == 'roman':
result.setFontStyleHint(QtGui.QFont.Times)
elif key == 'mono':
result.setFontStyleHint(QtGui.QFont.TypeWriter)
return result
def _get_brush(self, color):
""" Returns a brush for the color.
"""
result = self._brushes.get(color)
if result is None:
qcolor = self._get_color(color)
result = QtGui.QBrush(qcolor)
self._brushes[color] = result
return result
def _get_color(self, color):
""" Returns a QColor built from a Pygments color string.
"""
qcolor = QtGui.QColor()
qcolor.setRgb(int(color[:2], base=16),
int(color[2:4], base=16),
int(color[4:6], base=16))
return qcolor

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""QString compatibility."""
import sys
PY2 = sys.version[0] == '2'
def qstring_length(text):
"""
Tries to compute what the length of an utf16-encoded QString would be.
"""
if PY2:
# I don't know what this is encoded in, so there is nothing I can do.
return len(text)
utf16_text = text.encode('utf16')
length = len(utf16_text) // 2
# Remove Byte order mark.
# TODO: All unicode Non-characters should be removed
if utf16_text[:2] in [b'\xff\xfe', b'\xff\xff', b'\xfe\xff']:
length -= 1
return length

View file

@ -0,0 +1,461 @@
""" A minimal application using the Qt console-style Jupyter frontend.
This is not a complete console app, as subprocess will not be able to receive
input, there is no real readline support, among other limitations.
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import signal
import sys
from warnings import warn
# If run on Windows:
#
# 1. Install an exception hook which pops up a message box.
# Pythonw.exe hides the console, so without this the application
# silently fails to load.
#
# We always install this handler, because the expectation is for
# qtconsole to bring up a GUI even if called from the console.
# The old handler is called, so the exception is printed as well.
# If desired, check for pythonw with an additional condition
# (sys.executable.lower().find('pythonw.exe') >= 0).
#
# 2. Set AppUserModelID for Windows 7 and later so that qtconsole
# uses its assigned taskbar icon instead of grabbing the one with
# the same AppUserModelID
#
if os.name == 'nt':
# 1.
old_excepthook = sys.excepthook
# Exclude this from our autogenerated API docs.
undoc = lambda func: func
@undoc
def gui_excepthook(exctype, value, tb):
try:
import ctypes, traceback
MB_ICONERROR = 0x00000010
title = u'Error starting QtConsole'
msg = u''.join(traceback.format_exception(exctype, value, tb))
ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
finally:
# Also call the old exception hook to let it do
# its thing too.
old_excepthook(exctype, value, tb)
sys.excepthook = gui_excepthook
# 2.
try:
from ctypes import windll
windll.shell32.SetCurrentProcessExplicitAppUserModelID("Jupyter.Qtconsole")
except AttributeError:
pass
from qtpy import QtCore, QtGui, QtWidgets
from traitlets.config.application import boolean_flag
from traitlets.config.application import catch_config_error
from qtconsole.jupyter_widget import JupyterWidget
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtconsole import styles, __version__
from qtconsole.mainwindow import MainWindow
from qtconsole.client import QtKernelClient
from qtconsole.manager import QtKernelManager
from traitlets import (
Dict, Unicode, CBool, Any
)
from jupyter_core.application import JupyterApp, base_flags, base_aliases
from jupyter_client.consoleapp import (
JupyterConsoleApp, app_aliases, app_flags,
)
from jupyter_client.localinterfaces import is_local_ip
_examples = """
jupyter qtconsole # start the qtconsole
"""
#-----------------------------------------------------------------------------
# Aliases and Flags
#-----------------------------------------------------------------------------
# FIXME: workaround bug in jupyter_client < 4.1 excluding base_flags,aliases
flags = dict(base_flags)
qt_flags = {
'plain' : ({'JupyterQtConsoleApp' : {'plain' : True}},
"Disable rich text support."),
}
qt_flags.update(boolean_flag(
'banner', 'JupyterQtConsoleApp.display_banner',
"Display a banner upon starting the QtConsole.",
"Don't display a banner upon starting the QtConsole."
))
# and app_flags from the Console Mixin
qt_flags.update(app_flags)
# add frontend flags to the full set
flags.update(qt_flags)
# start with copy of base jupyter aliases
aliases = dict(base_aliases)
qt_aliases = dict(
style = 'JupyterWidget.syntax_style',
stylesheet = 'JupyterQtConsoleApp.stylesheet',
editor = 'JupyterWidget.editor',
paging = 'ConsoleWidget.paging',
)
# and app_aliases from the Console Mixin
qt_aliases.update(app_aliases)
qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
# add frontend aliases to the full set
aliases.update(qt_aliases)
# get flags&aliases into sets, and remove a couple that
# shouldn't be scrubbed from backend flags:
qt_aliases = set(qt_aliases.keys())
qt_flags = set(qt_flags.keys())
class JupyterQtConsoleApp(JupyterApp, JupyterConsoleApp):
name = 'jupyter-qtconsole'
version = __version__
description = """
The Jupyter QtConsole.
This launches a Console-style application using Qt. It is not a full
console, in that launched terminal subprocesses will not be able to accept
input.
"""
examples = _examples
classes = [JupyterWidget] + JupyterConsoleApp.classes
flags = Dict(flags)
aliases = Dict(aliases)
frontend_flags = Any(qt_flags)
frontend_aliases = Any(qt_aliases)
kernel_client_class = QtKernelClient
kernel_manager_class = QtKernelManager
stylesheet = Unicode('', config=True,
help="path to a custom CSS stylesheet")
hide_menubar = CBool(False, config=True,
help="Start the console window with the menu bar hidden.")
maximize = CBool(False, config=True,
help="Start the console window maximized.")
plain = CBool(False, config=True,
help="Use a plaintext widget instead of rich text (plain can't print/save).")
display_banner = CBool(True, config=True,
help="Whether to display a banner upon starting the QtConsole."
)
def _plain_changed(self, name, old, new):
kind = 'plain' if new else 'rich'
self.config.ConsoleWidget.kind = kind
if new:
self.widget_factory = JupyterWidget
else:
self.widget_factory = RichJupyterWidget
# the factory for creating a widget
widget_factory = Any(RichJupyterWidget)
def parse_command_line(self, argv=None):
super(JupyterQtConsoleApp, self).parse_command_line(argv)
self.build_kernel_argv(self.extra_args)
def new_frontend_master(self):
""" Create and return new frontend attached to new kernel, launched on localhost.
"""
kernel_manager = self.kernel_manager_class(
connection_file=self._new_connection_file(),
parent=self,
autorestart=True,
)
# start the kernel
kwargs = {}
# FIXME: remove special treatment of IPython kernels
if self.kernel_manager.ipykernel:
kwargs['extra_arguments'] = self.kernel_argv
kernel_manager.start_kernel(**kwargs)
kernel_manager.client_factory = self.kernel_client_class
kernel_client = kernel_manager.client()
kernel_client.start_channels(shell=True, iopub=True)
widget = self.widget_factory(config=self.config,
local_kernel=True)
self.init_colors(widget)
widget.kernel_manager = kernel_manager
widget.kernel_client = kernel_client
widget._existing = False
widget._may_close = True
widget._confirm_exit = self.confirm_exit
widget._display_banner = self.display_banner
return widget
def new_frontend_connection(self, connection_file):
"""Create and return a new frontend attached to an existing kernel.
Parameters
----------
connection_file : str
The connection_file path this frontend is to connect to
"""
kernel_client = self.kernel_client_class(
connection_file=connection_file,
config=self.config,
)
kernel_client.load_connection_file()
kernel_client.start_channels()
widget = self.widget_factory(config=self.config,
local_kernel=False)
self.init_colors(widget)
widget._existing = True
widget._may_close = False
widget._confirm_exit = False
widget._display_banner = self.display_banner
widget.kernel_client = kernel_client
widget.kernel_manager = None
return widget
def new_frontend_slave(self, current_widget):
"""Create and return a new frontend attached to an existing kernel.
Parameters
----------
current_widget : JupyterWidget
The JupyterWidget whose kernel this frontend is to share
"""
kernel_client = self.kernel_client_class(
connection_file=current_widget.kernel_client.connection_file,
config = self.config,
)
kernel_client.load_connection_file()
kernel_client.start_channels()
widget = self.widget_factory(config=self.config,
local_kernel=False)
self.init_colors(widget)
widget._existing = True
widget._may_close = False
widget._confirm_exit = False
widget._display_banner = self.display_banner
widget.kernel_client = kernel_client
widget.kernel_manager = current_widget.kernel_manager
return widget
def init_qt_app(self):
# separate from qt_elements, because it must run first
self.app = QtWidgets.QApplication(['jupyter-qtconsole'])
self.app.setApplicationName('jupyter-qtconsole')
def init_qt_elements(self):
# Create the widget.
base_path = os.path.abspath(os.path.dirname(__file__))
icon_path = os.path.join(base_path, 'resources', 'icon', 'JupyterConsole.svg')
self.app.icon = QtGui.QIcon(icon_path)
QtWidgets.QApplication.setWindowIcon(self.app.icon)
ip = self.ip
local_kernel = (not self.existing) or is_local_ip(ip)
self.widget = self.widget_factory(config=self.config,
local_kernel=local_kernel)
self.init_colors(self.widget)
self.widget._existing = self.existing
self.widget._may_close = not self.existing
self.widget._confirm_exit = self.confirm_exit
self.widget._display_banner = self.display_banner
self.widget.kernel_manager = self.kernel_manager
self.widget.kernel_client = self.kernel_client
self.window = MainWindow(self.app,
confirm_exit=self.confirm_exit,
new_frontend_factory=self.new_frontend_master,
slave_frontend_factory=self.new_frontend_slave,
connection_frontend_factory=self.new_frontend_connection,
)
self.window.log = self.log
self.window.add_tab_with_frontend(self.widget)
self.window.init_menu_bar()
# Ignore on OSX, where there is always a menu bar
if sys.platform != 'darwin' and self.hide_menubar:
self.window.menuBar().setVisible(False)
self.window.setWindowTitle('Jupyter QtConsole')
def init_colors(self, widget):
"""Configure the coloring of the widget"""
# Note: This will be dramatically simplified when colors
# are removed from the backend.
# parse the colors arg down to current known labels
cfg = self.config
colors = cfg.ZMQInteractiveShell.colors if 'ZMQInteractiveShell.colors' in cfg else None
style = cfg.JupyterWidget.syntax_style if 'JupyterWidget.syntax_style' in cfg else None
sheet = cfg.JupyterWidget.style_sheet if 'JupyterWidget.style_sheet' in cfg else None
# find the value for colors:
if colors:
colors=colors.lower()
if colors in ('lightbg', 'light'):
colors='lightbg'
elif colors in ('dark', 'linux'):
colors='linux'
else:
colors='nocolor'
elif style:
if style=='bw':
colors='nocolor'
elif styles.dark_style(style):
colors='linux'
else:
colors='lightbg'
else:
colors=None
# Configure the style
if style:
widget.style_sheet = styles.sheet_from_template(style, colors)
widget.syntax_style = style
widget._syntax_style_changed()
widget._style_sheet_changed()
elif colors:
# use a default dark/light/bw style
widget.set_default_style(colors=colors)
if self.stylesheet:
# we got an explicit stylesheet
if os.path.isfile(self.stylesheet):
with open(self.stylesheet) as f:
sheet = f.read()
else:
raise IOError("Stylesheet %r not found." % self.stylesheet)
if sheet:
widget.style_sheet = sheet
widget._style_sheet_changed()
def init_signal(self):
"""allow clean shutdown on sigint"""
signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
# need a timer, so that QApplication doesn't block until a real
# Qt event fires (can require mouse movement)
# timer trick from http://stackoverflow.com/q/4938723/938949
timer = QtCore.QTimer()
# Let the interpreter run each 200 ms:
timer.timeout.connect(lambda: None)
timer.start(200)
# hold onto ref, so the timer doesn't get cleaned up
self._sigint_timer = timer
def _deprecate_config(self, cfg, old_name, new_name):
"""Warn about deprecated config."""
if old_name in cfg:
self.log.warning(
"Use %s in config, not %s. Outdated config:\n %s",
new_name, old_name,
'\n '.join(
'{name}.{key} = {value!r}'.format(key=key, value=value,
name=old_name)
for key, value in self.config[old_name].items()
)
)
cfg = cfg.copy()
cfg[new_name].merge(cfg[old_name])
return cfg
def _init_asyncio_patch(self):
"""
Same workaround fix as https://github.com/ipython/ipykernel/pull/456
Set default asyncio policy to be compatible with tornado
Tornado 6 (at least) is not compatible with the default
asyncio implementation on Windows
Pick the older SelectorEventLoopPolicy on Windows
if the known-incompatible default policy is in use.
do this as early as possible to make it a low priority and overrideable
ref: https://github.com/tornadoweb/tornado/issues/2608
FIXME: if/when tornado supports the defaults in asyncio,
remove and bump tornado requirement for py38
"""
if sys.platform.startswith("win") and sys.version_info >= (3, 8):
import asyncio
try:
from asyncio import (
WindowsProactorEventLoopPolicy,
WindowsSelectorEventLoopPolicy,
)
except ImportError:
pass
# not affected
else:
if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:
# WindowsProactorEventLoopPolicy is not compatible with tornado 6
# fallback to the pre-3.8 default of Selector
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
@catch_config_error
def initialize(self, argv=None):
self._init_asyncio_patch()
self.init_qt_app()
super(JupyterQtConsoleApp, self).initialize(argv)
if self._dispatching:
return
# handle deprecated renames
for old_name, new_name in [
('IPythonQtConsoleApp', 'JupyterQtConsole'),
('IPythonWidget', 'JupyterWidget'),
('RichIPythonWidget', 'RichJupyterWidget'),
]:
cfg = self._deprecate_config(self.config, old_name, new_name)
if cfg:
self.update_config(cfg)
JupyterConsoleApp.initialize(self,argv)
self.init_qt_elements()
self.init_signal()
def start(self):
super(JupyterQtConsoleApp, self).start()
# draw the window
if self.maximize:
self.window.showMaximized()
else:
self.window.show()
self.window.raise_()
# Start the application main loop.
self.app.exec_()
class IPythonQtConsoleApp(JupyterQtConsoleApp):
def __init__(self, *a, **kw):
warn("IPythonQtConsoleApp is deprecated; use JupyterQtConsoleApp",
DeprecationWarning)
super(IPythonQtConsoleApp, self).__init__(*a, **kw)
# -----------------------------------------------------------------------------
# Main entry point
# -----------------------------------------------------------------------------
def main():
JupyterQtConsoleApp.launch_instance()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,569 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="512"
height="512"
id="svg2"
version="1.1"
inkscape:version="0.48.2 r9819"
sodipodi:docname="JupyterConsole.svg"
inkscape:export-filename="JupyterConsole.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4">
<linearGradient
id="linearGradient990">
<stop
id="stop992"
offset="0"
style="stop-color:#d4d4d4;stop-opacity:1;" />
<stop
style="stop-color:#f6f6f6;stop-opacity:1;"
offset="0.18783081"
id="stop998" />
<stop
style="stop-color:#a7a7a7;stop-opacity:1;"
offset="0.33046141"
id="stop994" />
<stop
id="stop1026"
offset="0.66523069"
style="stop-color:#919191;stop-opacity:1;" />
<stop
style="stop-color:#868686;stop-opacity:1;"
offset="0.83261538"
id="stop1028" />
<stop
id="stop1032"
offset="0.92357516"
style="stop-color:#868686;stop-opacity:1;" />
<stop
id="stop1030"
offset="0.96787697"
style="stop-color:#aaaaaa;stop-opacity:1;" />
<stop
id="stop996"
offset="1"
style="stop-color:#c2c2c2;stop-opacity:1;" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient1621">
<stop
style="stop-color:#d4d4d4;stop-opacity:1;"
offset="0"
id="stop1623" />
<stop
style="stop-color:#d4d4d4;stop-opacity:0;"
offset="1"
id="stop1625" />
</linearGradient>
<linearGradient
id="linearGradient826">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop828" />
<stop
style="stop-color:#ffffff;stop-opacity:0.69512194;"
offset="1"
id="stop830" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient826"
id="linearGradient832"
x1="105.70982"
y1="518.53571"
x2="757.14288"
y2="248.53572"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7551453,0,0,0.7551453,57.364381,318.43926)" />
<style
id="style1439"
type="text/css">
@font-face { font-family:&quot;Inconsolata&quot;;src:url(&quot;#FontID0&quot;) format(svg)}
.fil0 {fill:#1F1A17}
.fil2 {fill:#006633}
.fil1 {fill:#1F1A17}
.fnt1 {font-weight:500;font-size:3.5278;font-family:'Inconsolata'}
.fnt0 {font-weight:500;font-size:6.35;font-family:'Inconsolata'}
</style>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1621"
id="linearGradient1631"
gradientUnits="userSpaceOnUse"
x1="390.46347"
y1="712.64929"
x2="389.88318"
y2="764.16711"
gradientTransform="matrix(0.7551453,0,0,0.7551453,57.364381,318.43922)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient990"
id="linearGradient870"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7551453,0,0,0.7551453,57.364381,318.43926)"
x1="336.14798"
y1="18.710255"
x2="336.14798"
y2="66.858391" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient990"
id="linearGradient1012"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7551453,0,0,0.7551453,57.364381,318.43926)"
x1="291.68039"
y1="511.74365"
x2="291.68039"
y2="564.10553" />
<filter
inkscape:collect="always"
id="filter988">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="5.9071426"
id="feGaussianBlur990" />
</filter>
<linearGradient
id="linearGradient4689">
<stop
style="stop-color:#5a9fd4;stop-opacity:1"
offset="0"
id="stop4691" />
<stop
style="stop-color:#306998;stop-opacity:1"
offset="1"
id="stop4693" />
</linearGradient>
<filter
inkscape:collect="always"
id="filter3988">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="2.9780484"
id="feGaussianBlur3990" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4125"
gradientUnits="userSpaceOnUse"
x1="323.06018"
y1="147.10051"
x2="464.48874"
y2="269.24338" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4127"
gradientUnits="userSpaceOnUse"
x1="323.06018"
y1="147.10051"
x2="464.48874"
y2="269.24338" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4129"
gradientUnits="userSpaceOnUse"
x1="323.06018"
y1="147.10051"
x2="464.48874"
y2="269.24338" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4131"
gradientUnits="userSpaceOnUse"
x1="323.06018"
y1="147.10051"
x2="464.48874"
y2="269.24338" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4133"
gradientUnits="userSpaceOnUse"
x1="323.06018"
y1="147.10051"
x2="464.48874"
y2="269.24338" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4135"
gradientUnits="userSpaceOnUse"
x1="323.06018"
y1="147.10051"
x2="464.48874"
y2="269.24338" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4137"
gradientUnits="userSpaceOnUse"
x1="323.06018"
y1="147.10051"
x2="464.48874"
y2="269.24338" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4139"
gradientUnits="userSpaceOnUse"
x1="486.50031"
y1="184.54053"
x2="496.16876"
y2="248.36336" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4141"
gradientUnits="userSpaceOnUse"
x1="486.50031"
y1="184.54053"
x2="496.16876"
y2="248.36336" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4143"
gradientUnits="userSpaceOnUse"
x1="485.7803"
y1="185.98055"
x2="496.88876"
y2="249.08336" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4145"
gradientUnits="userSpaceOnUse"
x1="485.7803"
y1="185.98055"
x2="496.88876"
y2="249.08336" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4147"
gradientUnits="userSpaceOnUse"
x1="484.3403"
y1="182.38054"
x2="495.44876"
y2="243.32335" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4149"
gradientUnits="userSpaceOnUse"
x1="484.3403"
y1="182.38054"
x2="495.44876"
y2="243.32335" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4689"
id="linearGradient4151"
gradientUnits="userSpaceOnUse"
x1="323.06018"
y1="147.10051"
x2="147.68851"
y2="293.00339" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="327.50118"
inkscape:cy="215.30649"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1245"
inkscape:window-height="675"
inkscape:window-x="47"
inkscape:window-y="0"
inkscape:window-maximized="0" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-55.203036,-282.24337)">
<rect
style="opacity:0.41800005;color:#000000;fill:#020202;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter988);enable-background:accumulate"
id="rect1032"
width="628.57141"
height="552.85712"
x="76.46875"
y="220.12053"
rx="0"
ry="0"
transform="matrix(0.76259826,0,0,0.76259826,12.765793,164.57423)" />
<rect
y="332.22418"
x="71.162964"
height="415.55746"
width="473.45871"
id="rect1629"
style="color:#000000;fill:url(#linearGradient1631);fill-opacity:1;fill-rule:nonzero;stroke:#5b5b5b;stroke-width:1.51029062;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
y="332.95441"
x="71.774574"
height="38.836063"
width="472.50522"
id="rect12"
style="color:#000000;fill:url(#linearGradient870);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
inkscape:connector-curvature="0"
id="rect797"
d="m 71.774575,708.36947 472.505205,0 0,38.83606 -472.505205,0 z"
style="color:#000000;fill:url(#linearGradient1012);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
style="color:#000000;fill:#0c212d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect10"
width="472.50522"
height="338.7366"
x="71.774574"
y="369.63287" />
<path
style="opacity:0.231;color:#000000;fill:url(#linearGradient832);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 71.771204,369.62234 0,31.90891 0,1.86734 0,210.74249 C 185.0871,551.67384 349.48037,510.52371 535.90238,506.04065 c 2.79464,-0.0672 5.58165,-0.11401 8.37739,-0.16416 l 0,-102.4779 0,-1.86734 0,-31.90891 -472.508566,0 z"
id="rect793"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path1030"
d="m 71.774575,374.48737 472.505205,0 0,-4.85448 -472.505205,0 z"
style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;opacity:0.75362319" />
<path
style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1px;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;opacity:0.76086957"
d="m 71.774575,708.36947 472.505205,0 0,-4.85448 -472.505205,0 z"
id="path1024"
inkscape:connector-curvature="0" />
<g
id="g4082"
transform="translate(0,4)">
<g
style="filter:url(#filter3988)"
id="g3972"
transform="matrix(0.99206275,0,0,0.99206275,13.445202,326.71769)">
<g
style="fill:url(#linearGradient4137);fill-opacity:1"
id="g3974">
<g
id="text3976"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4127);fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4066"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4125);fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 355.96093,272.06912 -38.13242,0 0,-128.9858 38.13242,0 0,10.76086 -24.98829,0 0,107.39186 24.98829,0 0,10.83308"
inkscape:connector-curvature="0" />
</g>
<g
id="text3978"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4131);fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4069"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4129);fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 439.12013,261.23604 24.98829,0 0,-107.39186 -24.98829,0 0,-10.76086 38.13242,0 0,128.9858 -38.13242,0 0,-10.83308"
inkscape:connector-curvature="0" />
</g>
<g
id="text3980"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4135);fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4072"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4133);fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 514.87478,165.86889 c 6.11462,8e-5 9.17195,3.3463 9.17201,10.03865 -6e-5,6.6925 -3.05739,10.03871 -9.17201,10.03865 -6.1147,6e-5 -9.17203,-3.34615 -9.172,-10.03865 -3e-5,-6.69235 3.0573,-10.03857 9.172,-10.03865 m 0,63.26515 c 6.11462,2e-5 9.17195,3.34623 9.17201,10.03865 -6e-5,6.74058 -3.05739,10.11087 -9.17201,10.11087 -6.1147,0 -9.17203,-3.37029 -9.172,-10.11087 -3e-5,-6.69242 3.0573,-10.03863 9.172,-10.03865"
inkscape:connector-curvature="0" />
</g>
</g>
<g
style="fill:#ffffff"
id="g3982">
<g
id="text3984"
style="font-size:204.03166199px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4075"
style="font-size:204.03166199px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 151.39749,272.06912 -77.308871,0 0,-12.25385 29.389331,-1.9925 0,-117.1588 -29.389331,-1.9925 0,-12.25386 77.308871,0 0,12.25386 -29.2897,1.9925 0,117.1588 29.2897,1.9925 0,12.25385"
inkscape:connector-curvature="0" />
<path
id="path4077"
style="font-size:204.03166199px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 210.07652,215.38259 0,56.68653 -18.53022,0 0,-145.65151 40.24843,0 c 34.13802,1.5e-4 51.20706,14.21328 51.20716,42.63943 -10e-5,14.54532 -4.61605,25.90254 -13.84785,34.0717 -9.16557,8.16929 -22.51528,12.25391 -40.04918,12.25385 l -19.02834,0 m 0,-15.74072 16.93622,0 c 13.15041,7e-5 22.54834,-2.39092 28.19383,-7.17299 5.71173,-4.78191 8.56764,-12.25376 8.56773,-22.41559 -9e-5,-18.5301 -11.22448,-27.7952 -33.67319,-27.79533 l -20.02459,0 0,57.38391"
inkscape:connector-curvature="0" />
</g>
<g
id="text3986"
style="font-size:131.4621582px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4080"
style="font-size:131.4621582px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 367.00874,170.0062 12.06781,0 16.81792,41.9806 c 3.50904,8.77272 5.41336,14.97779 5.71295,18.61524 l 0.38515,0 c 0.98421,-4.79287 2.90992,-11.04074 5.77714,-18.74363 l 15.34153,-41.85221 12.13201,0 -30.49049,79.66042 c -2.86722,7.44609 -6.20512,13.03065 -10.01372,16.75373 -3.80867,3.76581 -9.07228,5.64873 -15.79087,5.64876 -3.68027,-3e-5 -7.27493,-0.36378 -10.784,-1.09124 l 0,-9.30762 c 2.6532,0.5135 5.56316,0.77026 8.72991,0.77028 4.10817,-2e-5 7.2963,-0.87729 9.56438,-2.63181 2.31083,-1.75455 4.36493,-4.77151 6.16229,-9.05086 l 3.72305,-9.62857 -29.33506,-71.12309"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
<g
style="fill:#000000"
id="g3992"
transform="matrix(0.99206275,0,0,0.99206275,15.645477,328.21773)">
<g
style="fill:#000000;fill-opacity:1"
id="g3994">
<g
id="text3996"
style="font-size:147.90756226px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4049"
style="font-size:147.90756226px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 355.96093,272.06912 -38.13242,0 0,-128.9858 38.13242,0 0,10.76086 -24.98829,0 0,107.39186 24.98829,0 0,10.83308"
inkscape:connector-curvature="0" />
</g>
<g
id="text3998"
style="font-size:147.90756226px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4052"
style="font-size:147.90756226px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 439.12013,261.23604 24.98829,0 0,-107.39186 -24.98829,0 0,-10.76086 38.13242,0 0,128.9858 -38.13242,0 0,-10.83308"
inkscape:connector-curvature="0" />
</g>
<g
id="text4000"
style="font-size:147.90756226px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4055"
style="font-size:147.90756226px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 514.87478,165.86889 c 6.11462,8e-5 9.17195,3.3463 9.17201,10.03865 -6e-5,6.6925 -3.05739,10.03871 -9.17201,10.03865 -6.1147,6e-5 -9.17203,-3.34615 -9.172,-10.03865 -3e-5,-6.69235 3.0573,-10.03857 9.172,-10.03865 m 0,63.26515 c 6.11462,2e-5 9.17195,3.34623 9.17201,10.03865 -6e-5,6.74058 -3.05739,10.11087 -9.17201,10.11087 -6.1147,0 -9.17203,-3.37029 -9.172,-10.11087 -3e-5,-6.69242 3.0573,-10.03863 9.172,-10.03865"
inkscape:connector-curvature="0" />
</g>
</g>
<g
style="fill:#000000"
id="g4002">
<g
id="text4004"
style="font-size:204.03166199px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4058"
style="font-size:204.03166199px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 151.39749,272.06912 -77.308871,0 0,-12.25385 29.389331,-1.9925 0,-117.1588 -29.389331,-1.9925 0,-12.25386 77.308871,0 0,12.25386 -29.2897,1.9925 0,117.1588 29.2897,1.9925 0,12.25385"
inkscape:connector-curvature="0" />
<path
id="path4060"
style="font-size:204.03166199px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 210.07652,215.38259 0,56.68653 -18.53022,0 0,-145.65151 40.24843,0 c 34.13802,1.5e-4 51.20706,14.21328 51.20716,42.63943 -10e-5,14.54532 -4.61605,25.90254 -13.84785,34.0717 -9.16557,8.16929 -22.51528,12.25391 -40.04918,12.25385 l -19.02834,0 m 0,-15.74072 16.93622,0 c 13.15041,7e-5 22.54834,-2.39092 28.19383,-7.17299 5.71173,-4.78191 8.56764,-12.25376 8.56773,-22.41559 -9e-5,-18.5301 -11.22448,-27.7952 -33.67319,-27.79533 l -20.02459,0 0,57.38391"
inkscape:connector-curvature="0" />
</g>
<g
id="text4006"
style="font-size:131.4621582px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4063"
style="font-size:131.4621582px;font-weight:normal;fill:#000000;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 367.00874,170.0062 12.06781,0 16.81792,41.9806 c 3.50904,8.77272 5.41336,14.97779 5.71295,18.61524 l 0.38515,0 c 0.98421,-4.79287 2.90992,-11.04074 5.77714,-18.74363 l 15.34153,-41.85221 12.13201,0 -30.49049,79.66042 c -2.86722,7.44609 -6.20512,13.03065 -10.01372,16.75373 -3.80867,3.76581 -9.07228,5.64873 -15.79087,5.64876 -3.68027,-3e-5 -7.27493,-0.36378 -10.784,-1.09124 l 0,-9.30762 c 2.6532,0.5135 5.56316,0.77026 8.72991,0.77028 4.10817,-2e-5 7.2963,-0.87729 9.56438,-2.63181 2.31083,-1.75455 4.36493,-4.77151 6.16229,-9.05086 l 3.72305,-9.62857 -29.33506,-71.12309"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
<g
transform="matrix(0.99206275,0,0,0.99206275,13.445202,326.71769)"
style="fill:url(#linearGradient4151);fill-opacity:1"
id="g3938">
<g
id="text111"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4141);fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4037"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4139);fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 355.96093,272.06912 -38.13242,0 0,-128.9858 38.13242,0 0,10.76086 -24.98829,0 0,107.39186 24.98829,0 0,10.83308"
inkscape:connector-curvature="0" />
</g>
<g
id="text113"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4145);fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4043"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4143);fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 439.12013,261.23604 24.98829,0 0,-107.39186 -24.98829,0 0,-10.76086 38.13242,0 0,128.9858 -38.13242,0 0,-10.83308"
inkscape:connector-curvature="0" />
</g>
<g
id="text115"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4149);fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4046"
style="font-size:147.90756226px;font-weight:normal;fill:url(#linearGradient4147);fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 514.87478,165.86889 c 6.11462,8e-5 9.17195,3.3463 9.17201,10.03865 -6e-5,6.6925 -3.05739,10.03871 -9.17201,10.03865 -6.1147,6e-5 -9.17203,-3.34615 -9.172,-10.03865 -3e-5,-6.69235 3.0573,-10.03857 9.172,-10.03865 m 0,63.26515 c 6.11462,2e-5 9.17195,3.34623 9.17201,10.03865 -6e-5,6.74058 -3.05739,10.11087 -9.17201,10.11087 -6.1147,0 -9.17203,-3.37029 -9.172,-10.11087 -3e-5,-6.69242 3.0573,-10.03863 9.172,-10.03865"
inkscape:connector-curvature="0" />
</g>
</g>
<g
transform="matrix(0.99206275,0,0,0.99206275,13.445202,326.71769)"
style="fill:#ffffff"
id="g3945">
<g
id="text109"
style="font-size:204.03166199px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4032"
style="font-size:204.03166199px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 151.39749,272.06912 -77.308871,0 0,-12.25385 29.389331,-1.9925 0,-117.1588 -29.389331,-1.9925 0,-12.25386 77.308871,0 0,12.25386 -29.2897,1.9925 0,117.1588 29.2897,1.9925 0,12.25385"
inkscape:connector-curvature="0" />
<path
id="path4034"
style="font-size:204.03166199px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 210.07652,215.38259 0,56.68653 -18.53022,0 0,-145.65151 40.24843,0 c 34.13802,1.5e-4 51.20706,14.21328 51.20716,42.63943 -10e-5,14.54532 -4.61605,25.90254 -13.84785,34.0717 -9.16557,8.16929 -22.51528,12.25391 -40.04918,12.25385 l -19.02834,0 m 0,-15.74072 16.93622,0 c 13.15041,7e-5 22.54834,-2.39092 28.19383,-7.17299 5.71173,-4.78191 8.56764,-12.25376 8.56773,-22.41559 -9e-5,-18.5301 -11.22448,-27.7952 -33.67319,-27.79533 l -20.02459,0 0,57.38391"
inkscape:connector-curvature="0" />
</g>
<g
id="text117"
style="font-size:131.4621582px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono">
<path
id="path4040"
style="font-size:131.4621582px;font-weight:normal;fill:#ffffff;fill-rule:evenodd;font-family:Droid Sans Mono"
d="m 367.00874,170.0062 12.06781,0 16.81792,41.9806 c 3.50904,8.77272 5.41336,14.97779 5.71295,18.61524 l 0.38515,0 c 0.98421,-4.79287 2.90992,-11.04074 5.77714,-18.74363 l 15.34153,-41.85221 12.13201,0 -30.49049,79.66042 c -2.86722,7.44609 -6.20512,13.03065 -10.01372,16.75373 -3.80867,3.76581 -9.07228,5.64873 -15.79087,5.64876 -3.68027,-3e-5 -7.27493,-0.36378 -10.784,-1.09124 l 0,-9.30762 c 2.6532,0.5135 5.56316,0.77026 8.72991,0.77028 4.10817,-2e-5 7.2963,-0.87729 9.56438,-2.63181 2.31083,-1.75455 4.36493,-4.77151 6.16229,-9.05086 l 3.72305,-9.62857 -29.33506,-71.12309"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,4 @@
import warnings
warnings.warn("qtconsole.rich_ipython_widget is deprecated; "
"use qtconsole.rich_jupyter_widget", DeprecationWarning)
from .rich_jupyter_widget import *

View file

@ -0,0 +1,418 @@
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
from base64 import b64decode
import os
import re
from warnings import warn
from qtpy import QtCore, QtGui, QtWidgets
from ipython_genutils.path import ensure_dir_exists
from traitlets import Bool
from qtconsole.svg import save_svg, svg_to_clipboard, svg_to_image
from .jupyter_widget import JupyterWidget
try:
from IPython.lib.latextools import latex_to_png
except ImportError:
latex_to_png = None
class LatexError(Exception):
"""Exception for Latex errors"""
class RichIPythonWidget(JupyterWidget):
"""Dummy class for config inheritance. Destroyed below."""
class RichJupyterWidget(RichIPythonWidget):
""" An JupyterWidget that supports rich text, including lists, images, and
tables. Note that raw performance will be reduced compared to the plain
text version.
"""
# RichJupyterWidget protected class variables.
_payload_source_plot = 'ipykernel.pylab.backend_payload.add_plot_payload'
_jpg_supported = Bool(False)
# Used to determine whether a given html export attempt has already
# displayed a warning about being unable to convert a png to svg.
_svg_warning_displayed = False
#---------------------------------------------------------------------------
# 'object' interface
#---------------------------------------------------------------------------
def __init__(self, *args, **kw):
""" Create a RichJupyterWidget.
"""
kw['kind'] = 'rich'
super(RichJupyterWidget, self).__init__(*args, **kw)
# Configure the ConsoleWidget HTML exporter for our formats.
self._html_exporter.image_tag = self._get_image_tag
# Dictionary for resolving document resource names to SVG data.
self._name_to_svg_map = {}
# Do we support jpg ?
# it seems that sometime jpg support is a plugin of QT, so try to assume
# it is not always supported.
self._jpg_supported = 'jpeg' in QtGui.QImageReader.supportedImageFormats()
#---------------------------------------------------------------------------
# 'ConsoleWidget' public interface overides
#---------------------------------------------------------------------------
def export_html(self):
""" Shows a dialog to export HTML/XML in various formats.
Overridden in order to reset the _svg_warning_displayed flag prior
to the export running.
"""
self._svg_warning_displayed = False
super(RichJupyterWidget, self).export_html()
#---------------------------------------------------------------------------
# 'ConsoleWidget' protected interface
#---------------------------------------------------------------------------
def _context_menu_make(self, pos):
""" Reimplemented to return a custom context menu for images.
"""
format = self._control.cursorForPosition(pos).charFormat()
name = format.stringProperty(QtGui.QTextFormat.ImageName)
if name:
menu = QtWidgets.QMenu(self)
menu.addAction('Copy Image', lambda: self._copy_image(name))
menu.addAction('Save Image As...', lambda: self._save_image(name))
menu.addSeparator()
svg = self._name_to_svg_map.get(name, None)
if svg is not None:
menu.addSeparator()
menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
menu.addAction('Save SVG As...',
lambda: save_svg(svg, self._control))
else:
menu = super(RichJupyterWidget, self)._context_menu_make(pos)
return menu
#---------------------------------------------------------------------------
# 'BaseFrontendMixin' abstract interface
#---------------------------------------------------------------------------
def _pre_image_append(self, msg, prompt_number):
"""Append the Out[] prompt and make the output nicer
Shared code for some the following if statement
"""
self._append_plain_text(self.output_sep, True)
self._append_html(self._make_out_prompt(prompt_number), True)
self._append_plain_text('\n', True)
def _handle_execute_result(self, msg):
"""Overridden to handle rich data types, like SVG."""
self.log.debug("execute_result: %s", msg.get('content', ''))
if self.include_output(msg):
self.flush_clearoutput()
content = msg['content']
prompt_number = content.get('execution_count', 0)
data = content['data']
metadata = msg['content']['metadata']
if 'image/svg+xml' in data:
self._pre_image_append(msg, prompt_number)
self._append_svg(data['image/svg+xml'], True)
self._append_html(self.output_sep2, True)
elif 'image/png' in data:
self._pre_image_append(msg, prompt_number)
png = b64decode(data['image/png'].encode('ascii'))
self._append_png(png, True, metadata=metadata.get('image/png',
None))
self._append_html(self.output_sep2, True)
elif 'image/jpeg' in data and self._jpg_supported:
self._pre_image_append(msg, prompt_number)
jpg = b64decode(data['image/jpeg'].encode('ascii'))
self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg',
None))
self._append_html(self.output_sep2, True)
elif 'text/latex' in data:
self._pre_image_append(msg, prompt_number)
try:
self._append_latex(data['text/latex'], True)
except LatexError:
return super(RichJupyterWidget, self)._handle_display_data(msg)
self._append_html(self.output_sep2, True)
else:
# Default back to the plain text representation.
return super(RichJupyterWidget, self)._handle_execute_result(msg)
def _handle_display_data(self, msg):
"""Overridden to handle rich data types, like SVG."""
self.log.debug("display_data: %s", msg.get('content', ''))
if self.include_output(msg):
self.flush_clearoutput()
data = msg['content']['data']
metadata = msg['content']['metadata']
# Try to use the svg or html representations.
# FIXME: Is this the right ordering of things to try?
self.log.debug("display: %s", msg.get('content', ''))
if 'image/svg+xml' in data:
svg = data['image/svg+xml']
self._append_svg(svg, True)
elif 'image/png' in data:
# PNG data is base64 encoded as it passes over the network
# in a JSON structure so we decode it.
png = b64decode(data['image/png'].encode('ascii'))
self._append_png(png, True, metadata=metadata.get('image/png', None))
elif 'image/jpeg' in data and self._jpg_supported:
jpg = b64decode(data['image/jpeg'].encode('ascii'))
self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
elif 'text/latex' in data and latex_to_png:
try:
self._append_latex(data['text/latex'], True)
except LatexError:
return super(RichJupyterWidget, self)._handle_display_data(msg)
else:
# Default back to the plain text representation.
return super(RichJupyterWidget, self)._handle_display_data(msg)
#---------------------------------------------------------------------------
# 'RichJupyterWidget' protected interface
#---------------------------------------------------------------------------
def _is_latex_math(self, latex):
"""
Determine if a Latex string is in math mode
This is the only mode supported by qtconsole
"""
basic_envs = ['math', 'displaymath']
starable_envs = ['equation', 'eqnarray' 'multline', 'gather', 'align',
'flalign', 'alignat']
star_envs = [env + '*' for env in starable_envs]
envs = basic_envs + starable_envs + star_envs
env_syntax = [r'\begin{{{0}}} \end{{{0}}}'.format(env).split() for env in envs]
math_syntax = [
(r'\[', r'\]'), (r'\(', r'\)'),
('$$', '$$'), ('$', '$'),
]
for start, end in math_syntax + env_syntax:
inner = latex[len(start):-len(end)]
if start in inner or end in inner:
return False
if latex.startswith(start) and latex.endswith(end):
return True
return False
def _append_latex(self, latex, before_prompt=False, metadata=None):
""" Append latex data to the widget."""
png = None
if self._is_latex_math(latex):
png = latex_to_png(latex, wrap=False, backend='dvipng')
# Matplotlib only supports strings enclosed in dollar signs
if png is None and latex.startswith('$') and latex.endswith('$'):
# To avoid long and ugly errors, like the one reported in
# spyder-ide/spyder#7619
try:
png = latex_to_png(latex, wrap=False, backend='matplotlib')
except Exception:
pass
if png:
self._append_png(png, before_prompt, metadata)
else:
raise LatexError
def _append_jpg(self, jpg, before_prompt=False, metadata=None):
""" Append raw JPG data to the widget."""
self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
def _append_png(self, png, before_prompt=False, metadata=None):
""" Append raw PNG data to the widget.
"""
self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
def _append_svg(self, svg, before_prompt=False):
""" Append raw SVG data to the widget.
"""
self._append_custom(self._insert_svg, svg, before_prompt)
def _add_image(self, image):
""" Adds the specified QImage to the document and returns a
QTextImageFormat that references it.
"""
document = self._control.document()
name = str(image.cacheKey())
document.addResource(QtGui.QTextDocument.ImageResource,
QtCore.QUrl(name), image)
format = QtGui.QTextImageFormat()
format.setName(name)
return format
def _copy_image(self, name):
""" Copies the ImageResource with 'name' to the clipboard.
"""
image = self._get_image(name)
QtWidgets.QApplication.clipboard().setImage(image)
def _get_image(self, name):
""" Returns the QImage stored as the ImageResource with 'name'.
"""
document = self._control.document()
image = document.resource(QtGui.QTextDocument.ImageResource,
QtCore.QUrl(name))
return image
def _get_image_tag(self, match, path = None, format = "png"):
""" Return (X)HTML mark-up for the image-tag given by match.
Parameters
----------
match : re.SRE_Match
A match to an HTML image tag as exported by Qt, with
match.group("Name") containing the matched image ID.
path : string|None, optional [default None]
If not None, specifies a path to which supporting files may be
written (e.g., for linked images). If None, all images are to be
included inline.
format : "png"|"svg"|"jpg", optional [default "png"]
Format for returned or referenced images.
"""
if format in ("png","jpg"):
try:
image = self._get_image(match.group("name"))
except KeyError:
return "<b>Couldn't find image %s</b>" % match.group("name")
if path is not None:
ensure_dir_exists(path)
relpath = os.path.basename(path)
if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
"PNG"):
return '<img src="%s/qt_img%s.%s">' % (relpath,
match.group("name"),format)
else:
return "<b>Couldn't save image!</b>"
else:
ba = QtCore.QByteArray()
buffer_ = QtCore.QBuffer(ba)
buffer_.open(QtCore.QIODevice.WriteOnly)
image.save(buffer_, format.upper())
buffer_.close()
return '<img src="data:image/%s;base64,\n%s\n" />' % (
format,re.sub(r'(.{60})',r'\1\n', str(ba.toBase64().data().decode())))
elif format == "svg":
try:
svg = str(self._name_to_svg_map[match.group("name")])
except KeyError:
if not self._svg_warning_displayed:
QtWidgets.QMessageBox.warning(self, 'Error converting PNG to SVG.',
'Cannot convert PNG images to SVG, export with PNG figures instead. '
'If you want to export matplotlib figures as SVG, add '
'to your ipython config:\n\n'
'\tc.InlineBackend.figure_format = \'svg\'\n\n'
'And regenerate the figures.',
QtWidgets.QMessageBox.Ok)
self._svg_warning_displayed = True
return ("<b>Cannot convert PNG images to SVG.</b> "
"You must export this session with PNG images. "
"If you want to export matplotlib figures as SVG, add to your config "
"<span>c.InlineBackend.figure_format = 'svg'</span> "
"and regenerate the figures.")
# Not currently checking path, because it's tricky to find a
# cross-browser way to embed external SVG images (e.g., via
# object or embed tags).
# Chop stand-alone header from matplotlib SVG
offset = svg.find("<svg")
assert(offset > -1)
return svg[offset:]
else:
return '<b>Unrecognized image format</b>'
def _insert_jpg(self, cursor, jpg, metadata=None):
""" Insert raw PNG data into the widget."""
self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
def _insert_png(self, cursor, png, metadata=None):
""" Insert raw PNG data into the widget.
"""
self._insert_img(cursor, png, 'png', metadata=metadata)
def _insert_img(self, cursor, img, fmt, metadata=None):
""" insert a raw image, jpg or png """
if metadata:
width = metadata.get('width', None)
height = metadata.get('height', None)
else:
width = height = None
try:
image = QtGui.QImage()
image.loadFromData(img, fmt.upper())
if width and height:
image = image.scaled(width, height,
QtCore.Qt.IgnoreAspectRatio,
QtCore.Qt.SmoothTransformation)
elif width and not height:
image = image.scaledToWidth(width, QtCore.Qt.SmoothTransformation)
elif height and not width:
image = image.scaledToHeight(height, QtCore.Qt.SmoothTransformation)
except ValueError:
self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
else:
format = self._add_image(image)
cursor.insertBlock()
cursor.insertImage(format)
cursor.insertBlock()
def _insert_svg(self, cursor, svg):
""" Insert raw SVG data into the widet.
"""
try:
image = svg_to_image(svg)
except ValueError:
self._insert_plain_text(cursor, 'Received invalid SVG data.')
else:
format = self._add_image(image)
self._name_to_svg_map[format.name()] = svg
cursor.insertBlock()
cursor.insertImage(format)
cursor.insertBlock()
def _save_image(self, name, format='PNG'):
""" Shows a save dialog for the ImageResource with 'name'.
"""
dialog = QtWidgets.QFileDialog(self._control, 'Save Image')
dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
dialog.setDefaultSuffix(format.lower())
dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
if dialog.exec_():
filename = dialog.selectedFiles()[0]
image = self._get_image(name)
image.save(filename, format)
# Clobber RichIPythonWidget above:
class RichIPythonWidget(RichJupyterWidget):
"""Deprecated class. Use RichJupyterWidget."""
def __init__(self, *a, **kw):
warn("RichIPythonWidget is deprecated, use RichJupyterWidget",
DeprecationWarning)
super(RichIPythonWidget, self).__init__(*a, **kw)

View file

@ -0,0 +1,234 @@
""" Defines classes and functions for working with Qt's rich text system.
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import io
import os
import re
from qtpy import QtWidgets
from ipython_genutils import py3compat
#-----------------------------------------------------------------------------
# Constants
#-----------------------------------------------------------------------------
# A regular expression for an HTML paragraph with no content.
EMPTY_P_RE = re.compile(r'<p[^/>]*>\s*</p>')
# A regular expression for matching images in rich text HTML.
# Note that this is overly restrictive, but Qt's output is predictable...
IMG_RE = re.compile(r'<img src="(?P<name>[\d]+)" />')
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
class HtmlExporter(object):
""" A stateful HTML exporter for a Q(Plain)TextEdit.
This class is designed for convenient user interaction.
"""
def __init__(self, control):
""" Creates an HtmlExporter for the given Q(Plain)TextEdit.
"""
assert isinstance(control, (QtWidgets.QPlainTextEdit, QtWidgets.QTextEdit))
self.control = control
self.filename = 'ipython.html'
self.image_tag = None
self.inline_png = None
def export(self):
""" Displays a dialog for exporting HTML generated by Qt's rich text
system.
Returns
-------
The name of the file that was saved, or None if no file was saved.
"""
parent = self.control.window()
dialog = QtWidgets.QFileDialog(parent, 'Save as...')
dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
filters = [
'HTML with PNG figures (*.html *.htm)',
'XHTML with inline SVG figures (*.xhtml *.xml)'
]
dialog.setNameFilters(filters)
if self.filename:
dialog.selectFile(self.filename)
root,ext = os.path.splitext(self.filename)
if ext.lower() in ('.xml', '.xhtml'):
dialog.selectNameFilter(filters[-1])
if dialog.exec_():
self.filename = dialog.selectedFiles()[0]
choice = dialog.selectedNameFilter()
html = py3compat.cast_unicode(self.control.document().toHtml())
# Configure the exporter.
if choice.startswith('XHTML'):
exporter = export_xhtml
else:
# If there are PNGs, decide how to export them.
inline = self.inline_png
if inline is None and IMG_RE.search(html):
dialog = QtWidgets.QDialog(parent)
dialog.setWindowTitle('Save as...')
layout = QtWidgets.QVBoxLayout(dialog)
msg = "Exporting HTML with PNGs"
info = "Would you like inline PNGs (single large html " \
"file) or external image files?"
checkbox = QtWidgets.QCheckBox("&Don't ask again")
checkbox.setShortcut('D')
ib = QtWidgets.QPushButton("&Inline")
ib.setShortcut('I')
eb = QtWidgets.QPushButton("&External")
eb.setShortcut('E')
box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question,
dialog.windowTitle(), msg)
box.setInformativeText(info)
box.addButton(ib, QtWidgets.QMessageBox.NoRole)
box.addButton(eb, QtWidgets.QMessageBox.YesRole)
layout.setSpacing(0)
layout.addWidget(box)
layout.addWidget(checkbox)
dialog.setLayout(layout)
dialog.show()
reply = box.exec_()
dialog.hide()
inline = (reply == 0)
if checkbox.checkState():
# Don't ask anymore; always use this choice.
self.inline_png = inline
exporter = lambda h, f, i: export_html(h, f, i, inline)
# Perform the export!
try:
return exporter(html, self.filename, self.image_tag)
except Exception as e:
msg = "Error exporting HTML to %s\n" % self.filename + str(e)
reply = QtWidgets.QMessageBox.warning(parent, 'Error', msg,
QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok)
return None
#-----------------------------------------------------------------------------
# Functions
#-----------------------------------------------------------------------------
def export_html(html, filename, image_tag = None, inline = True):
""" Export the contents of the ConsoleWidget as HTML.
Parameters
----------
html : unicode,
A Python unicode string containing the Qt HTML to export.
filename : str
The file to be saved.
image_tag : callable, optional (default None)
Used to convert images. See ``default_image_tag()`` for information.
inline : bool, optional [default True]
If True, include images as inline PNGs. Otherwise, include them as
links to external PNG files, mimicking web browsers' "Web Page,
Complete" behavior.
"""
if image_tag is None:
image_tag = default_image_tag
if inline:
path = None
else:
root,ext = os.path.splitext(filename)
path = root + "_files"
if os.path.isfile(path):
raise OSError("%s exists, but is not a directory." % path)
with io.open(filename, 'w', encoding='utf-8') as f:
html = fix_html(html)
f.write(IMG_RE.sub(lambda x: image_tag(x, path = path, format = "png"),
html))
def export_xhtml(html, filename, image_tag=None):
""" Export the contents of the ConsoleWidget as XHTML with inline SVGs.
Parameters
----------
html : unicode,
A Python unicode string containing the Qt HTML to export.
filename : str
The file to be saved.
image_tag : callable, optional (default None)
Used to convert images. See ``default_image_tag()`` for information.
"""
if image_tag is None:
image_tag = default_image_tag
with io.open(filename, 'w', encoding='utf-8') as f:
# Hack to make xhtml header -- note that we are not doing any check for
# valid XML.
offset = html.find("<html>")
assert offset > -1, 'Invalid HTML string: no <html> tag.'
html = (u'<html xmlns="http://www.w3.org/1999/xhtml">\n'+
html[offset+6:])
html = fix_html(html)
f.write(IMG_RE.sub(lambda x: image_tag(x, path = None, format = "svg"),
html))
def default_image_tag(match, path = None, format = "png"):
""" Return (X)HTML mark-up for the image-tag given by match.
This default implementation merely removes the image, and exists mostly
for documentation purposes. More information than is present in the Qt
HTML is required to supply the images.
Parameters
----------
match : re.SRE_Match
A match to an HTML image tag as exported by Qt, with match.group("Name")
containing the matched image ID.
path : string|None, optional [default None]
If not None, specifies a path to which supporting files may be written
(e.g., for linked images). If None, all images are to be included
inline.
format : "png"|"svg", optional [default "png"]
Format for returned or referenced images.
"""
return u''
def fix_html(html):
""" Transforms a Qt-generated HTML string into a standards-compliant one.
Parameters
----------
html : unicode,
A Python unicode string containing the Qt HTML.
"""
# A UTF-8 declaration is needed for proper rendering of some characters
# (e.g., indented commands) when viewing exported HTML on a local system
# (i.e., without seeing an encoding declaration in an HTTP header).
# C.f. http://www.w3.org/International/O-charset for details.
offset = html.find('<head>')
if offset > -1:
html = (html[:offset+6]+
'\n<meta http-equiv="Content-Type" '+
'content="text/html; charset=utf-8" />\n'+
html[offset+6:])
# Replace empty paragraphs tags with line breaks.
html = re.sub(EMPTY_P_RE, '<br/>', html)
return html

View file

@ -0,0 +1,119 @@
""" Style utilities, templates, and defaults for syntax highlighting widgets.
"""
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
from colorsys import rgb_to_hls
from pygments.styles import get_style_by_name
from pygments.token import Token
#-----------------------------------------------------------------------------
# Constants
#-----------------------------------------------------------------------------
default_template = '''\
QPlainTextEdit, QTextEdit {
background-color: %(bgcolor)s;
background-clip: padding;
color: %(fgcolor)s;
selection-background-color: %(select)s;
}
.inverted {
background-color: %(fgcolor)s;
color: %(bgcolor)s;
}
.error { color: red; }
.in-prompt-number { font-weight: bold; }
.out-prompt-number { font-weight: bold; }
'''
# The default light style sheet: black text on a white background.
default_light_style_template = default_template + '''\
.in-prompt { color: navy; }
.out-prompt { color: darkred; }
'''
default_light_style_sheet = default_light_style_template%dict(
bgcolor='white', fgcolor='black', select="#ccc")
default_light_syntax_style = 'default'
# The default dark style sheet: white text on a black background.
default_dark_style_template = default_template + '''\
.in-prompt,
.in-prompt-number { color: lime; }
.out-prompt,
.out-prompt-number { color: red; }
'''
default_dark_style_sheet = default_dark_style_template%dict(
bgcolor='black', fgcolor='white', select="#555")
default_dark_syntax_style = 'monokai'
# The default monochrome
default_bw_style_sheet = default_template%dict(
bgcolor='white', fgcolor='black', select="#ccc")
default_bw_syntax_style = 'bw'
def hex_to_rgb(color):
"""Convert a hex color to rgb integer tuple."""
if color.startswith('#'):
color = color[1:]
if len(color) == 3:
color = ''.join([c*2 for c in color])
if len(color) != 6:
return False
try:
r = int(color[:2],16)
g = int(color[2:4],16)
b = int(color[4:],16)
except ValueError:
return False
else:
return r,g,b
def dark_color(color):
"""Check whether a color is 'dark'.
Currently, this is simply whether the luminance is <50%"""
rgb = hex_to_rgb(color)
if rgb:
return rgb_to_hls(*rgb)[1] < 128
else: # default to False
return False
def dark_style(stylename):
"""Guess whether the background of the style with name 'stylename'
counts as 'dark'."""
return dark_color(get_style_by_name(stylename).background_color)
def get_colors(stylename):
"""Construct the keys to be used building the base stylesheet
from a templatee."""
style = get_style_by_name(stylename)
fgcolor = style.style_for_token(Token.Text)['color'] or ''
if len(fgcolor) in (3,6):
# could be 'abcdef' or 'ace' hex, which needs '#' prefix
try:
int(fgcolor, 16)
except TypeError:
pass
else:
fgcolor = "#"+fgcolor
return dict(
bgcolor = style.background_color,
select = style.highlight_color,
fgcolor = fgcolor
)
def sheet_from_template(name, colors='lightbg'):
"""Use one of the base templates, and set bg/fg/select colors."""
colors = colors.lower()
if colors=='lightbg':
return default_light_style_template%get_colors(name)
elif colors=='linux':
return default_dark_style_template%get_colors(name)
elif colors=='nocolor':
return default_bw_style_sheet
else:
raise KeyError("No such color scheme: %s"%colors)

View file

@ -0,0 +1,92 @@
""" Defines utility functions for working with SVG documents in Qt.
"""
# System library imports.
from qtpy import QtCore, QtGui, QtSvg, QtWidgets
# Our own imports
from ipython_genutils.py3compat import unicode_type
def save_svg(string, parent=None):
""" Prompts the user to save an SVG document to disk.
Parameters
----------
string : basestring
A Python string containing a SVG document.
parent : QWidget, optional
The parent to use for the file dialog.
Returns
-------
The name of the file to which the document was saved, or None if the save
was cancelled.
"""
if isinstance(string, unicode_type):
string = string.encode('utf-8')
dialog = QtWidgets.QFileDialog(parent, 'Save SVG Document')
dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
dialog.setDefaultSuffix('svg')
dialog.setNameFilter('SVG document (*.svg)')
if dialog.exec_():
filename = dialog.selectedFiles()[0]
f = open(filename, 'wb')
try:
f.write(string)
finally:
f.close()
return filename
return None
def svg_to_clipboard(string):
""" Copy a SVG document to the clipboard.
Parameters
----------
string : basestring
A Python string containing a SVG document.
"""
if isinstance(string, unicode_type):
string = string.encode('utf-8')
mime_data = QtCore.QMimeData()
mime_data.setData('image/svg+xml', string)
QtWidgets.QApplication.clipboard().setMimeData(mime_data)
def svg_to_image(string, size=None):
""" Convert a SVG document to a QImage.
Parameters
----------
string : basestring
A Python string containing a SVG document.
size : QSize, optional
The size of the image that is produced. If not specified, the SVG
document's default size is used.
Raises
------
ValueError
If an invalid SVG string is provided.
Returns
-------
A QImage of format QImage.Format_ARGB32.
"""
if isinstance(string, unicode_type):
string = string.encode('utf-8')
renderer = QtSvg.QSvgRenderer(QtCore.QByteArray(string))
if not renderer.isValid():
raise ValueError('Invalid SVG data.')
if size is None:
size = renderer.defaultSize()
image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32)
image.fill(0)
painter = QtGui.QPainter(image)
renderer.render(painter)
return image

View file

@ -0,0 +1,6 @@
import os
import sys
no_display = (sys.platform not in ('darwin', 'win32') and
os.environ.get('DISPLAY', '') == '')

View file

@ -0,0 +1,610 @@
import sys
import unittest
from flaky import flaky
import pytest
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtTest import QTest
from qtconsole.console_widget import ConsoleWidget
from qtconsole.qtconsoleapp import JupyterQtConsoleApp
from . import no_display
if sys.version[0] == '2': # Python 2
from IPython.core.inputsplitter import InputSplitter as TransformerManager
else:
from IPython.core.inputtransformer2 import TransformerManager
SHELL_TIMEOUT = 20000
@pytest.fixture
def qtconsole(qtbot):
"""Qtconsole fixture."""
# Create a console
console = JupyterQtConsoleApp()
console.initialize(argv=[])
qtbot.addWidget(console.window)
console.window.confirm_exit = False
console.window.show()
return console
@flaky(max_runs=3)
@pytest.mark.parametrize(
"debug", [True, False])
def test_scroll(qtconsole, qtbot, debug):
"""
Make sure the scrolling works.
"""
window = qtconsole.window
shell = window.active_frontend
control = shell._control
scroll_bar = control.verticalScrollBar()
# Wait until the console is fully up
qtbot.waitUntil(lambda: shell._prompt_html is not None,
timeout=SHELL_TIMEOUT)
assert scroll_bar.value() == 0
# Define a function with loads of output
# Check the outputs are working as well
code = ["import time",
"def print_numbers():",
" for i in range(1000):",
" print(i)",
" time.sleep(.01)"]
for line in code:
qtbot.keyClicks(control, line)
qtbot.keyClick(control, QtCore.Qt.Key_Enter)
with qtbot.waitSignal(shell.executed):
qtbot.keyClick(control, QtCore.Qt.Key_Enter,
modifier=QtCore.Qt.ShiftModifier)
def run_line(line, block=True):
qtbot.keyClicks(control, line)
if block:
with qtbot.waitSignal(shell.executed):
qtbot.keyClick(control, QtCore.Qt.Key_Enter,
modifier=QtCore.Qt.ShiftModifier)
else:
qtbot.keyClick(control, QtCore.Qt.Key_Enter,
modifier=QtCore.Qt.ShiftModifier)
if debug:
# Enter debug
run_line('%debug print()', block=False)
qtbot.keyClick(control, QtCore.Qt.Key_Enter)
# redefine run_line
def run_line(line, block=True):
qtbot.keyClicks(control, '!' + line)
qtbot.keyClick(control, QtCore.Qt.Key_Enter,
modifier=QtCore.Qt.ShiftModifier)
if block:
qtbot.waitUntil(
lambda: control.toPlainText().strip(
).split()[-1] == "ipdb>")
prev_position = scroll_bar.value()
# Create a bunch of inputs
for i in range(20):
run_line('a = 1')
assert scroll_bar.value() > prev_position
# Put the scroll bar higher and check it doesn't move
prev_position = scroll_bar.value() + scroll_bar.pageStep() // 2
scroll_bar.setValue(prev_position)
for i in range(2):
run_line('a')
assert scroll_bar.value() == prev_position
# add more input and check it moved
for i in range(10):
run_line('a')
assert scroll_bar.value() > prev_position
prev_position = scroll_bar.value()
# Run the printing function
run_line('print_numbers()', block=False)
qtbot.wait(1000)
# Check everything advances
assert scroll_bar.value() > prev_position
# move up
prev_position = scroll_bar.value() - scroll_bar.pageStep()
scroll_bar.setValue(prev_position)
qtbot.wait(1000)
# Check position stayed the same
assert scroll_bar.value() == prev_position
# reset position
prev_position = scroll_bar.maximum() - (scroll_bar.pageStep() * 8) // 10
scroll_bar.setValue(prev_position)
qtbot.wait(1000)
assert scroll_bar.value() > prev_position
@flaky(max_runs=3)
def test_input(qtconsole, qtbot):
"""
Test input function
"""
window = qtconsole.window
shell = window.active_frontend
control = shell._control
# Wait until the console is fully up
qtbot.waitUntil(lambda: shell._prompt_html is not None,
timeout=SHELL_TIMEOUT)
with qtbot.waitSignal(shell.executed):
shell.execute("import time")
if sys.version[0] == '2':
input_function = 'raw_input'
else:
input_function = 'input'
shell.execute("print(" + input_function + "('name: ')); time.sleep(3)")
qtbot.waitUntil(lambda: control.toPlainText().split()[-1] == 'name:')
qtbot.keyClicks(control, 'test')
qtbot.keyClick(control, QtCore.Qt.Key_Enter)
qtbot.waitUntil(lambda: not shell._reading)
qtbot.keyClick(control, 'z', modifier=QtCore.Qt.ControlModifier)
for i in range(10):
qtbot.keyClick(control, QtCore.Qt.Key_Backspace)
qtbot.waitUntil(lambda: shell._prompt_html is not None,
timeout=SHELL_TIMEOUT)
assert 'name: test\ntest' in control.toPlainText()
@flaky(max_runs=3)
def test_debug(qtconsole, qtbot):
"""
Make sure the cursor works while debugging
It might not because the console is "_executing"
"""
window = qtconsole.window
shell = window.active_frontend
control = shell._control
# Wait until the console is fully up
qtbot.waitUntil(lambda: shell._prompt_html is not None,
timeout=SHELL_TIMEOUT)
# Enter execution
code = "%debug range(1)"
qtbot.keyClicks(control, code)
qtbot.keyClick(control, QtCore.Qt.Key_Enter,
modifier=QtCore.Qt.ShiftModifier)
qtbot.waitUntil(
lambda: control.toPlainText().strip().split()[-1] == "ipdb>",
timeout=SHELL_TIMEOUT)
# We should be able to move the cursor while debugging
qtbot.keyClicks(control, "abd")
qtbot.wait(100)
qtbot.keyClick(control, QtCore.Qt.Key_Left)
qtbot.keyClick(control, 'c')
qtbot.wait(100)
assert control.toPlainText().strip().split()[-1] == "abcd"
@pytest.mark.skipif(no_display, reason="Doesn't work without a display")
class TestConsoleWidget(unittest.TestCase):
@classmethod
def setUpClass(cls):
""" Create the application for the test case.
"""
cls._app = QtWidgets.QApplication.instance()
if cls._app is None:
cls._app = QtWidgets.QApplication([])
cls._app.setQuitOnLastWindowClosed(False)
@classmethod
def tearDownClass(cls):
""" Exit the application.
"""
QtWidgets.QApplication.quit()
def assert_text_equal(self, cursor, text):
cursor.select(cursor.Document)
selection = cursor.selectedText()
self.assertEqual(selection, text)
def test_special_characters(self):
""" Are special characters displayed correctly?
"""
w = ConsoleWidget()
cursor = w._get_prompt_cursor()
test_inputs = ['xyz\b\b=\n',
'foo\b\nbar\n',
'foo\b\nbar\r\n',
'abc\rxyz\b\b=']
expected_outputs = [u'x=z\u2029',
u'foo\u2029bar\u2029',
u'foo\u2029bar\u2029',
'x=z']
for i, text in enumerate(test_inputs):
w._insert_plain_text(cursor, text)
self.assert_text_equal(cursor, expected_outputs[i])
# clear all the text
cursor.insertText('')
def test_link_handling(self):
noKeys = QtCore.Qt
noButton = QtCore.Qt.MouseButton(0)
noButtons = QtCore.Qt.MouseButtons(0)
noModifiers = QtCore.Qt.KeyboardModifiers(0)
MouseMove = QtCore.QEvent.MouseMove
QMouseEvent = QtGui.QMouseEvent
w = ConsoleWidget()
cursor = w._get_prompt_cursor()
w._insert_html(cursor, '<a href="http://python.org">written in</a>')
obj = w._control
tip = QtWidgets.QToolTip
self.assertEqual(tip.text(), u'')
# should be somewhere else
elsewhereEvent = QMouseEvent(MouseMove, QtCore.QPoint(50,50),
noButton, noButtons, noModifiers)
w.eventFilter(obj, elsewhereEvent)
self.assertEqual(tip.isVisible(), False)
self.assertEqual(tip.text(), u'')
# should be over text
overTextEvent = QMouseEvent(MouseMove, QtCore.QPoint(1,5),
noButton, noButtons, noModifiers)
w.eventFilter(obj, overTextEvent)
self.assertEqual(tip.isVisible(), True)
self.assertEqual(tip.text(), "http://python.org")
# should still be over text
stillOverTextEvent = QMouseEvent(MouseMove, QtCore.QPoint(1,5),
noButton, noButtons, noModifiers)
w.eventFilter(obj, stillOverTextEvent)
self.assertEqual(tip.isVisible(), True)
self.assertEqual(tip.text(), "http://python.org")
def test_width_height(self):
# width()/height() QWidget properties should not be overridden.
w = ConsoleWidget()
self.assertEqual(w.width(), QtWidgets.QWidget.width(w))
self.assertEqual(w.height(), QtWidgets.QWidget.height(w))
def test_prompt_cursors(self):
"""Test the cursors that keep track of where the prompt begins and
ends"""
w = ConsoleWidget()
w._prompt = 'prompt>'
doc = w._control.document()
# Fill up the QTextEdit area with the maximum number of blocks
doc.setMaximumBlockCount(10)
for _ in range(9):
w._append_plain_text('line\n')
# Draw the prompt, this should cause the first lines to be deleted
w._show_prompt()
self.assertEqual(doc.blockCount(), 10)
# _prompt_pos should be at the end of the document
self.assertEqual(w._prompt_pos, w._get_end_pos())
# _append_before_prompt_pos should be at the beginning of the prompt
self.assertEqual(w._append_before_prompt_pos,
w._prompt_pos - len(w._prompt))
# insert some more text without drawing a new prompt
w._append_plain_text('line\n')
self.assertEqual(w._prompt_pos,
w._get_end_pos() - len('line\n'))
self.assertEqual(w._append_before_prompt_pos,
w._prompt_pos - len(w._prompt))
# redraw the prompt
w._show_prompt()
self.assertEqual(w._prompt_pos, w._get_end_pos())
self.assertEqual(w._append_before_prompt_pos,
w._prompt_pos - len(w._prompt))
# insert some text before the prompt
w._append_plain_text('line', before_prompt=True)
self.assertEqual(w._prompt_pos, w._get_end_pos())
self.assertEqual(w._append_before_prompt_pos,
w._prompt_pos - len(w._prompt))
def test_select_all(self):
w = ConsoleWidget()
w._append_plain_text('Header\n')
w._prompt = 'prompt>'
w._show_prompt()
control = w._control
app = QtWidgets.QApplication.instance()
cursor = w._get_cursor()
w._insert_plain_text_into_buffer(cursor, "if:\n pass")
cursor.clearSelection()
control.setTextCursor(cursor)
# "select all" action selects cell first
w.select_all_smart()
QTest.keyClick(control, QtCore.Qt.Key_C, QtCore.Qt.ControlModifier)
copied = app.clipboard().text()
self.assertEqual(copied, 'if:\n> pass')
# # "select all" action triggered a second time selects whole document
w.select_all_smart()
QTest.keyClick(control, QtCore.Qt.Key_C, QtCore.Qt.ControlModifier)
copied = app.clipboard().text()
self.assertEqual(copied, 'Header\nprompt>if:\n> pass')
def test_keypresses(self):
"""Test the event handling code for keypresses."""
w = ConsoleWidget()
w._append_plain_text('Header\n')
w._prompt = 'prompt>'
w._show_prompt()
app = QtWidgets.QApplication.instance()
control = w._control
# Test setting the input buffer
w._set_input_buffer('test input')
self.assertEqual(w._get_input_buffer(), 'test input')
# Ctrl+K kills input until EOL
w._set_input_buffer('test input')
c = control.textCursor()
c.setPosition(c.position() - 3)
control.setTextCursor(c)
QTest.keyClick(control, QtCore.Qt.Key_K, QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(), 'test in')
# Ctrl+V pastes
w._set_input_buffer('test input ')
app.clipboard().setText('pasted text')
QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(), 'test input pasted text')
self.assertEqual(control.document().blockCount(), 2)
# Paste should strip indentation
w._set_input_buffer('test input ')
app.clipboard().setText(' pasted text')
QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(), 'test input pasted text')
self.assertEqual(control.document().blockCount(), 2)
# Multiline paste, should also show continuation marks
w._set_input_buffer('test input ')
app.clipboard().setText('line1\nline2\nline3')
QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
'test input line1\nline2\nline3')
self.assertEqual(control.document().blockCount(), 4)
self.assertEqual(control.document().findBlockByNumber(1).text(),
'prompt>test input line1')
self.assertEqual(control.document().findBlockByNumber(2).text(),
'> line2')
self.assertEqual(control.document().findBlockByNumber(3).text(),
'> line3')
# Multiline paste should strip indentation intelligently
# in the case where pasted text has leading whitespace on first line
# and we're pasting into indented position
w._set_input_buffer(' ')
app.clipboard().setText(' If 1:\n pass')
QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
' If 1:\n pass')
# Ctrl+Backspace should intelligently remove the last word
w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n"
" 'bar', 'bar', 'bar']")
QTest.keyClick(control, QtCore.Qt.Key_Backspace,
QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
("foo = ['foo', 'foo', 'foo', \n"
" 'bar', 'bar', '"))
QTest.keyClick(control, QtCore.Qt.Key_Backspace,
QtCore.Qt.ControlModifier)
QTest.keyClick(control, QtCore.Qt.Key_Backspace,
QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
("foo = ['foo', 'foo', 'foo', \n"
" '"))
QTest.keyClick(control, QtCore.Qt.Key_Backspace,
QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
("foo = ['foo', 'foo', 'foo', \n"
""))
QTest.keyClick(control, QtCore.Qt.Key_Backspace,
QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
"foo = ['foo', 'foo', 'foo',")
# Ctrl+Delete should intelligently remove the next word
w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n"
" 'bar', 'bar', 'bar']")
c = control.textCursor()
c.setPosition(35)
control.setTextCursor(c)
QTest.keyClick(control, QtCore.Qt.Key_Delete,
QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
("foo = ['foo', 'foo', ', \n"
" 'bar', 'bar', 'bar']"))
QTest.keyClick(control, QtCore.Qt.Key_Delete,
QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
("foo = ['foo', 'foo', \n"
" 'bar', 'bar', 'bar']"))
QTest.keyClick(control, QtCore.Qt.Key_Delete,
QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
"foo = ['foo', 'foo', 'bar', 'bar', 'bar']")
w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n"
" 'bar', 'bar', 'bar']")
c = control.textCursor()
c.setPosition(48)
control.setTextCursor(c)
QTest.keyClick(control, QtCore.Qt.Key_Delete,
QtCore.Qt.ControlModifier)
self.assertEqual(w._get_input_buffer(),
("foo = ['foo', 'foo', 'foo', \n"
"'bar', 'bar', 'bar']"))
# Left and right keys should respect the continuation prompt
w._set_input_buffer("line 1\n"
"line 2\n"
"line 3")
c = control.textCursor()
c.setPosition(20) # End of line 1
control.setTextCursor(c)
QTest.keyClick(control, QtCore.Qt.Key_Right)
# Cursor should have moved after the continuation prompt
self.assertEqual(control.textCursor().position(), 23)
QTest.keyClick(control, QtCore.Qt.Key_Left)
# Cursor should have moved to the end of the previous line
self.assertEqual(control.textCursor().position(), 20)
# TODO: many more keybindings
def test_indent(self):
"""Test the event handling code for indent/dedent keypresses ."""
w = ConsoleWidget()
w._append_plain_text('Header\n')
w._prompt = 'prompt>'
w._show_prompt()
control = w._control
# TAB with multiline selection should block-indent
w._set_input_buffer("")
c = control.textCursor()
pos=c.position()
w._set_input_buffer("If 1:\n pass")
c.setPosition(pos, QtGui.QTextCursor.KeepAnchor)
control.setTextCursor(c)
QTest.keyClick(control, QtCore.Qt.Key_Tab)
self.assertEqual(w._get_input_buffer()," If 1:\n pass")
# TAB with multiline selection, should block-indent to next multiple
# of 4 spaces, if first line has 0 < indent < 4
w._set_input_buffer("")
c = control.textCursor()
pos=c.position()
w._set_input_buffer(" If 2:\n pass")
c.setPosition(pos, QtGui.QTextCursor.KeepAnchor)
control.setTextCursor(c)
QTest.keyClick(control, QtCore.Qt.Key_Tab)
self.assertEqual(w._get_input_buffer()," If 2:\n pass")
# Shift-TAB with multiline selection should block-dedent
w._set_input_buffer("")
c = control.textCursor()
pos=c.position()
w._set_input_buffer(" If 3:\n pass")
c.setPosition(pos, QtGui.QTextCursor.KeepAnchor)
control.setTextCursor(c)
QTest.keyClick(control, QtCore.Qt.Key_Backtab)
self.assertEqual(w._get_input_buffer(),"If 3:\n pass")
def test_complete(self):
class TestKernelClient(object):
def is_complete(self, source):
calls.append(source)
return msg_id
w = ConsoleWidget()
cursor = w._get_prompt_cursor()
w._execute = lambda *args: calls.append(args)
w.kernel_client = TestKernelClient()
msg_id = object()
calls = []
# test incomplete statement (no _execute called, but indent added)
w.execute("thing", interactive=True)
self.assertEqual(calls, ["thing"])
calls = []
w._handle_is_complete_reply(
dict(parent_header=dict(msg_id=msg_id),
content=dict(status="incomplete", indent="!!!")))
self.assert_text_equal(cursor, u"thing\u2029> !!!")
self.assertEqual(calls, [])
# test complete statement (_execute called)
msg_id = object()
w.execute("else", interactive=True)
self.assertEqual(calls, ["else"])
calls = []
w._handle_is_complete_reply(
dict(parent_header=dict(msg_id=msg_id),
content=dict(status="complete", indent="###")))
self.assertEqual(calls, [("else", False)])
calls = []
self.assert_text_equal(cursor, u"thing\u2029> !!!else\u2029")
# test missing answer from is_complete
msg_id = object()
w.execute("done", interactive=True)
self.assertEqual(calls, ["done"])
calls = []
self.assert_text_equal(cursor, u"thing\u2029> !!!else\u2029")
w._trigger_is_complete_callback()
self.assert_text_equal(cursor, u"thing\u2029> !!!else\u2029\u2029> ")
# assert that late answer isn't destroying anything
w._handle_is_complete_reply(
dict(parent_header=dict(msg_id=msg_id),
content=dict(status="complete", indent="###")))
self.assertEqual(calls, [])
def test_complete_python(self):
"""Test that is_complete is working correctly for Python."""
# Kernel client to test the responses of is_complete
class TestIPyKernelClient(object):
def is_complete(self, source):
tm = TransformerManager()
check_complete = tm.check_complete(source)
responses.append(check_complete)
# Initialize widget
responses = []
w = ConsoleWidget()
w._append_plain_text('Header\n')
w._prompt = 'prompt>'
w._show_prompt()
w.kernel_client = TestIPyKernelClient()
# Execute incomplete statement inside a block
code = '\n'.join(["if True:", " a = 1"])
w._set_input_buffer(code)
w.execute(interactive=True)
assert responses == [('incomplete', 4)]
# Execute complete statement inside a block
responses = []
code = '\n'.join(["if True:", " a = 1\n\n"])
w._set_input_buffer(code)
w.execute(interactive=True)
assert responses == [('complete', None)]

View file

@ -0,0 +1,179 @@
# Standard library imports
import unittest
# Local imports
from qtconsole.ansi_code_processor import AnsiCodeProcessor
class TestAnsiCodeProcessor(unittest.TestCase):
def setUp(self):
self.processor = AnsiCodeProcessor()
def test_clear(self):
""" Do control sequences for clearing the console work?
"""
string = '\x1b[2J\x1b[K'
i = -1
for i, substring in enumerate(self.processor.split_string(string)):
if i == 0:
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'erase')
self.assertEqual(action.area, 'screen')
self.assertEqual(action.erase_to, 'all')
elif i == 1:
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'erase')
self.assertEqual(action.area, 'line')
self.assertEqual(action.erase_to, 'end')
else:
self.fail('Too many substrings.')
self.assertEqual(i, 1, 'Too few substrings.')
def test_colors(self):
""" Do basic controls sequences for colors work?
"""
string = 'first\x1b[34mblue\x1b[0mlast'
i = -1
for i, substring in enumerate(self.processor.split_string(string)):
if i == 0:
self.assertEqual(substring, 'first')
self.assertEqual(self.processor.foreground_color, None)
elif i == 1:
self.assertEqual(substring, 'blue')
self.assertEqual(self.processor.foreground_color, 4)
elif i == 2:
self.assertEqual(substring, 'last')
self.assertEqual(self.processor.foreground_color, None)
else:
self.fail('Too many substrings.')
self.assertEqual(i, 2, 'Too few substrings.')
def test_colors_xterm(self):
""" Do xterm-specific control sequences for colors work?
"""
string = '\x1b]4;20;rgb:ff/ff/ff\x1b' \
'\x1b]4;25;rgbi:1.0/1.0/1.0\x1b'
substrings = list(self.processor.split_string(string))
desired = { 20 : (255, 255, 255),
25 : (255, 255, 255) }
self.assertEqual(self.processor.color_map, desired)
string = '\x1b[38;5;20m\x1b[48;5;25m'
substrings = list(self.processor.split_string(string))
self.assertEqual(self.processor.foreground_color, 20)
self.assertEqual(self.processor.background_color, 25)
def test_true_color(self):
"""Do 24bit True Color control sequences?
"""
string = '\x1b[38;2;255;100;0m\x1b[48;2;100;100;100m'
substrings = list(self.processor.split_string(string))
self.assertEqual(self.processor.foreground_color, [255, 100, 0])
self.assertEqual(self.processor.background_color, [100, 100, 100])
def test_scroll(self):
""" Do control sequences for scrolling the buffer work?
"""
string = '\x1b[5S\x1b[T'
i = -1
for i, substring in enumerate(self.processor.split_string(string)):
if i == 0:
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'scroll')
self.assertEqual(action.dir, 'up')
self.assertEqual(action.unit, 'line')
self.assertEqual(action.count, 5)
elif i == 1:
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'scroll')
self.assertEqual(action.dir, 'down')
self.assertEqual(action.unit, 'line')
self.assertEqual(action.count, 1)
else:
self.fail('Too many substrings.')
self.assertEqual(i, 1, 'Too few substrings.')
def test_formfeed(self):
""" Are formfeed characters processed correctly?
"""
string = '\f' # form feed
self.assertEqual(list(self.processor.split_string(string)), [''])
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'scroll')
self.assertEqual(action.dir, 'down')
self.assertEqual(action.unit, 'page')
self.assertEqual(action.count, 1)
def test_carriage_return(self):
""" Are carriage return characters processed correctly?
"""
string = 'foo\rbar' # carriage return
splits = []
actions = []
for split in self.processor.split_string(string):
splits.append(split)
actions.append([action.action for action in self.processor.actions])
self.assertEqual(splits, ['foo', None, 'bar'])
self.assertEqual(actions, [[], ['carriage-return'], []])
def test_carriage_return_newline(self):
"""transform CRLF to LF"""
string = 'foo\rbar\r\ncat\r\n\n' # carriage return and newline
# only one CR action should occur, and '\r\n' should transform to '\n'
splits = []
actions = []
for split in self.processor.split_string(string):
splits.append(split)
actions.append([action.action for action in self.processor.actions])
self.assertEqual(splits, ['foo', None, 'bar', '\r\n', 'cat', '\r\n', '\n'])
self.assertEqual(actions, [[], ['carriage-return'], [], ['newline'], [], ['newline'], ['newline']])
def test_beep(self):
""" Are beep characters processed correctly?
"""
string = 'foo\abar' # bell
splits = []
actions = []
for split in self.processor.split_string(string):
splits.append(split)
actions.append([action.action for action in self.processor.actions])
self.assertEqual(splits, ['foo', None, 'bar'])
self.assertEqual(actions, [[], ['beep'], []])
def test_backspace(self):
""" Are backspace characters processed correctly?
"""
string = 'foo\bbar' # backspace
splits = []
actions = []
for split in self.processor.split_string(string):
splits.append(split)
actions.append([action.action for action in self.processor.actions])
self.assertEqual(splits, ['foo', None, 'bar'])
self.assertEqual(actions, [[], ['backspace'], []])
def test_combined(self):
""" Are CR and BS characters processed correctly in combination?
BS is treated as a change in print position, rather than a
backwards character deletion. Therefore a BS at EOL is
effectively ignored.
"""
string = 'abc\rdef\b' # CR and backspace
splits = []
actions = []
for split in self.processor.split_string(string):
splits.append(split)
actions.append([action.action for action in self.processor.actions])
self.assertEqual(splits, ['abc', None, 'def', None])
self.assertEqual(actions, [[], ['carriage-return'], [], ['backspace']])
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,31 @@
"""Test QtConsoleApp"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import sys
from subprocess import check_output
from jupyter_core import paths
import pytest
from traitlets.tests.utils import check_help_all_output
from . import no_display
@pytest.mark.skipif(no_display, reason="Doesn't work without a display")
def test_help_output():
"""jupyter qtconsole --help-all works"""
check_help_all_output('qtconsole')
@pytest.mark.skipif(no_display, reason="Doesn't work without a display")
@pytest.mark.skipif(os.environ.get('CI', None) is None,
reason="Doesn't work outside of our CIs")
def test_generate_config():
"""jupyter qtconsole --generate-config"""
config_dir = paths.jupyter_config_dir()
check_output([sys.executable, '-m', 'qtconsole', '--generate-config'])
assert os.path.isfile(os.path.join(config_dir,
'jupyter_qtconsole_config.py'))

View file

@ -0,0 +1,162 @@
import time
import sys
import unittest
from ipython_genutils.py3compat import PY3
from jupyter_client.blocking.channels import Empty
from qtconsole.manager import QtKernelManager
PY2 = sys.version[0] == '2'
if PY2:
TimeoutError = RuntimeError
class Tests(unittest.TestCase):
def setUp(self):
"""Open a kernel."""
self.kernel_manager = QtKernelManager()
self.kernel_manager.start_kernel()
self.kernel_client = self.kernel_manager.client()
self.kernel_client.start_channels(shell=True, iopub=True)
self.blocking_client = self.kernel_client.blocking_client()
self.blocking_client.start_channels(shell=True, iopub=True)
self.comm_manager = self.kernel_client.comm_manager
# Check if client is working
self.blocking_client.execute('print(0)')
try:
self._get_next_msg()
self._get_next_msg()
except TimeoutError:
# Maybe it works now?
self.blocking_client.execute('print(0)')
self._get_next_msg()
self._get_next_msg()
def tearDown(self):
"""Close the kernel."""
if self.kernel_manager:
self.kernel_manager.shutdown_kernel(now=True)
if self.kernel_client:
self.kernel_client.shutdown()
def _get_next_msg(self, timeout=10):
# Get status messages
timeout_time = time.time() + timeout
msg_type = 'status'
while msg_type == 'status':
if timeout_time < time.time():
raise TimeoutError
try:
msg = self.blocking_client.get_iopub_msg(timeout=3)
msg_type = msg['header']['msg_type']
except Empty:
pass
return msg
def test_kernel_to_frontend(self):
"""Communicate from the kernel to the frontend."""
comm_manager = self.comm_manager
blocking_client = self.blocking_client
class DummyCommHandler():
def __init__(self):
comm_manager.register_target('test_api', self.comm_open)
self.last_msg = None
def comm_open(self, comm, msg):
comm.on_msg(self.comm_message)
comm.on_close(self.comm_message)
self.last_msg = msg['content']['data']
self.comm = comm
def comm_message(self, msg):
self.last_msg = msg['content']['data']
handler = DummyCommHandler()
blocking_client.execute(
"from ipykernel.comm import Comm\n"
"comm = Comm(target_name='test_api', data='open')\n"
"comm.send('message')\n"
"comm.close('close')\n"
"del comm\n"
"print('Done')\n"
)
# Get input
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'execute_input'
# Open comm
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'comm_open'
comm_manager._dispatch(msg)
assert handler.last_msg == 'open'
assert handler.comm.comm_id == msg['content']['comm_id']
# Get message
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'comm_msg'
comm_manager._dispatch(msg)
assert handler.last_msg == 'message'
assert handler.comm.comm_id == msg['content']['comm_id']
# Get close
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'comm_close'
comm_manager._dispatch(msg)
assert handler.last_msg == 'close'
assert handler.comm.comm_id == msg['content']['comm_id']
# Get close
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'stream'
def test_frontend_to_kernel(self):
"""Communicate from the frontend to the kernel."""
comm_manager = self.comm_manager
blocking_client = self.blocking_client
blocking_client.execute(
"class DummyCommHandler():\n"
" def __init__(self):\n"
" get_ipython().kernel.comm_manager.register_target(\n"
" 'test_api', self.comm_open)\n"
" def comm_open(self, comm, msg):\n"
" comm.on_msg(self.comm_message)\n"
" comm.on_close(self.comm_message)\n"
" print(msg['content']['data'])\n"
" def comm_message(self, msg):\n"
" print(msg['content']['data'])\n"
"dummy = DummyCommHandler()\n"
)
# Get input
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'execute_input'
# Open comm
comm = comm_manager.new_comm('test_api', data='open')
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'stream'
assert msg['content']['text'] == 'open\n'
# Get message
comm.send('message')
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'stream'
assert msg['content']['text'] == 'message\n'
# Get close
comm.close('close')
msg = self._get_next_msg()
# Received message has a header and parent header. The parent header has
# the info about the close message type in Python 3
if PY3:
assert msg['parent_header']['msg_type'] == 'comm_close'
assert msg['msg_type'] == 'stream'
assert msg['content']['text'] == 'close\n'
else:
# For some reason ipykernel notifies me that it is closing,
# even though I closed the comm
assert msg['header']['msg_type'] == 'comm_close'
assert comm.comm_id == msg['content']['comm_id']
msg = self._get_next_msg()
assert msg['header']['msg_type'] == 'stream'
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
import os
import tempfile
import shutil
import unittest
import pytest
from qtpy import QtCore, QtWidgets
from qtpy.QtTest import QTest
from qtconsole.console_widget import ConsoleWidget
from qtconsole.completion_widget import CompletionWidget
from . import no_display
class TemporaryDirectory(object):
"""
Context manager for tempfile.mkdtemp().
This class is available in python +v3.2.
See: https://gist.github.com/cpelley/10e2eeaf60dacc7956bb
"""
def __enter__(self):
self.dir_name = tempfile.mkdtemp()
return self.dir_name
def __exit__(self, exc_type, exc_value, traceback):
shutil.rmtree(self.dir_name)
TemporaryDirectory = getattr(tempfile, 'TemporaryDirectory',
TemporaryDirectory)
@pytest.mark.skipif(no_display, reason="Doesn't work without a display")
class TestCompletionWidget(unittest.TestCase):
@classmethod
def setUpClass(cls):
""" Create the application for the test case.
"""
cls._app = QtWidgets.QApplication.instance()
if cls._app is None:
cls._app = QtWidgets.QApplication([])
cls._app.setQuitOnLastWindowClosed(False)
@classmethod
def tearDownClass(cls):
""" Exit the application.
"""
QtWidgets.QApplication.quit()
def setUp(self):
""" Create the main widgets (ConsoleWidget)
"""
self.console = ConsoleWidget()
self.text_edit = self.console._control
def test_droplist_completer_shows(self):
w = CompletionWidget(self.console)
w.show_items(self.text_edit.textCursor(), ["item1", "item2", "item3"])
self.assertTrue(w.isVisible())
def test_droplist_completer_keyboard(self):
w = CompletionWidget(self.console)
w.show_items(self.text_edit.textCursor(), ["item1", "item2", "item3"])
QTest.keyClick(w, QtCore.Qt.Key_PageDown)
QTest.keyClick(w, QtCore.Qt.Key_Enter)
self.assertEqual(self.text_edit.toPlainText(), "item3")
def test_droplist_completer_mousepick(self):
leftButton = QtCore.Qt.LeftButton
w = CompletionWidget(self.console)
w.show_items(self.text_edit.textCursor(), ["item1", "item2", "item3"])
QTest.mouseClick(w.viewport(), leftButton, pos=QtCore.QPoint(19, 8))
QTest.mouseRelease(w.viewport(), leftButton, pos=QtCore.QPoint(19, 8))
QTest.mouseDClick(w.viewport(), leftButton, pos=QtCore.QPoint(19, 8))
self.assertEqual(self.text_edit.toPlainText(), "item1")
self.assertFalse(w.isVisible())
def test_common_path_complete(self):
with TemporaryDirectory() as tmpdir:
items = [
os.path.join(tmpdir, "common/common1/item1"),
os.path.join(tmpdir, "common/common1/item2"),
os.path.join(tmpdir, "common/common1/item3")]
for item in items:
os.makedirs(item)
w = CompletionWidget(self.console)
w.show_items(self.text_edit.textCursor(), items)
self.assertEqual(w.currentItem().text(), '/item1')
QTest.keyClick(w, QtCore.Qt.Key_Down)
self.assertEqual(w.currentItem().text(), '/item2')
QTest.keyClick(w, QtCore.Qt.Key_Down)
self.assertEqual(w.currentItem().text(), '/item3')

View file

@ -0,0 +1,96 @@
import unittest
import pytest
from qtpy import QtWidgets
from qtconsole.frontend_widget import FrontendWidget
from qtpy.QtTest import QTest
from . import no_display
@pytest.mark.skipif(no_display, reason="Doesn't work without a display")
class TestFrontendWidget(unittest.TestCase):
@classmethod
def setUpClass(cls):
""" Create the application for the test case.
"""
cls._app = QtWidgets.QApplication.instance()
if cls._app is None:
cls._app = QtWidgets.QApplication([])
cls._app.setQuitOnLastWindowClosed(False)
@classmethod
def tearDownClass(cls):
""" Exit the application.
"""
QtWidgets.QApplication.quit()
def test_transform_classic_prompt(self):
""" Test detecting classic prompts.
"""
w = FrontendWidget(kind='rich')
t = w._highlighter.transform_classic_prompt
# Base case
self.assertEqual(t('>>> test'), 'test')
self.assertEqual(t(' >>> test'), 'test')
self.assertEqual(t('\t >>> test'), 'test')
# No prompt
self.assertEqual(t(''), '')
self.assertEqual(t('test'), 'test')
# Continuation prompt
self.assertEqual(t('... test'), 'test')
self.assertEqual(t(' ... test'), 'test')
self.assertEqual(t(' ... test'), 'test')
self.assertEqual(t('\t ... test'), 'test')
# Prompts that don't match the 'traditional' prompt
self.assertEqual(t('>>>test'), '>>>test')
self.assertEqual(t('>> test'), '>> test')
self.assertEqual(t('...test'), '...test')
self.assertEqual(t('.. test'), '.. test')
# Prefix indicating input from other clients
self.assertEqual(t('[remote] >>> test'), 'test')
# Random other prefix
self.assertEqual(t('[foo] >>> test'), '[foo] >>> test')
def test_transform_ipy_prompt(self):
""" Test detecting IPython prompts.
"""
w = FrontendWidget(kind='rich')
t = w._highlighter.transform_ipy_prompt
# In prompt
self.assertEqual(t('In [1]: test'), 'test')
self.assertEqual(t('In [2]: test'), 'test')
self.assertEqual(t('In [10]: test'), 'test')
self.assertEqual(t(' In [1]: test'), 'test')
self.assertEqual(t('\t In [1]: test'), 'test')
# No prompt
self.assertEqual(t(''), '')
self.assertEqual(t('test'), 'test')
# Continuation prompt
self.assertEqual(t(' ...: test'), 'test')
self.assertEqual(t(' ...: test'), 'test')
self.assertEqual(t(' ...: test'), 'test')
self.assertEqual(t('\t ...: test'), 'test')
# Prompts that don't match the in-prompt
self.assertEqual(t('In [1]:test'), 'In [1]:test')
self.assertEqual(t('[1]: test'), '[1]: test')
self.assertEqual(t('In: test'), 'In: test')
self.assertEqual(t(': test'), ': test')
self.assertEqual(t('...: test'), '...: test')
# Prefix indicating input from other clients
self.assertEqual(t('[remote] In [1]: test'), 'test')
# Random other prefix
self.assertEqual(t('[foo] In [1]: test'), '[foo] In [1]: test')

View file

@ -0,0 +1,81 @@
import unittest
import pytest
from qtpy import QtWidgets
from qtconsole.client import QtKernelClient
from qtconsole.jupyter_widget import JupyterWidget
from . import no_display
from qtpy.QtTest import QTest
@pytest.mark.skipif(no_display, reason="Doesn't work without a display")
class TestJupyterWidget(unittest.TestCase):
@classmethod
def setUpClass(cls):
""" Create the application for the test case.
"""
cls._app = QtWidgets.QApplication.instance()
if cls._app is None:
cls._app = QtWidgets.QApplication([])
cls._app.setQuitOnLastWindowClosed(False)
@classmethod
def tearDownClass(cls):
""" Exit the application.
"""
QtWidgets.QApplication.quit()
def test_stylesheet_changed(self):
""" Test changing stylesheets.
"""
w = JupyterWidget(kind='rich')
# By default, the background is light. White text is rendered as black
self.assertEqual(w._ansi_processor.get_color(15).name(), '#000000')
# Change to a dark colorscheme. White text is rendered as white
w.syntax_style = 'monokai'
self.assertEqual(w._ansi_processor.get_color(15).name(), '#ffffff')
def test_other_output(self):
""" Test displaying output from other clients.
"""
w = JupyterWidget(kind='rich')
w._append_plain_text('Header\n')
w._show_interpreter_prompt(1)
w.other_output_prefix = '[other] '
w.syntax_style = 'default'
control = w._control
document = control.document()
msg = dict(
execution_count=1,
code='a = 1 + 1\nb = range(10)',
)
w._append_custom(w._insert_other_input, msg, before_prompt=True)
self.assertEqual(document.blockCount(), 6)
self.assertEqual(document.toPlainText(), (
u'Header\n'
u'\n'
u'[other] In [1]: a = 1 + 1\n'
u' ...: b = range(10)\n'
u'\n'
u'In [2]: '
))
# Check proper syntax highlighting
self.assertEqual(document.toHtml(), (
u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">\n'
u'<html><head><meta name="qrichtext" content="1" /><style type="text/css">\n'
u'p, li { white-space: pre-wrap; }\n'
u'</style></head><body style=" font-family:\'Monospace\'; font-size:9pt; font-weight:400; font-style:normal;">\n'
u'<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Header</p>\n'
u'<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>\n'
u'<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" color:#000080;">[other] In [</span><span style=" font-weight:600; color:#000080;">1</span><span style=" color:#000080;">]:</span> a = 1 + 1</p>\n'
u'<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" color:#000080;">\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0...:</span> b = range(10)</p>\n'
u'<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>\n'
u'<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" color:#000080;">In [</span><span style=" font-weight:600; color:#000080;">2</span><span style=" color:#000080;">]:</span> </p></body></html>'
))

View file

@ -0,0 +1,85 @@
import unittest
import pytest
from qtpy import QtGui, QtWidgets
from qtconsole.kill_ring import KillRing, QtKillRing
from . import no_display
@pytest.mark.skipif(no_display, reason="Doesn't work without a display")
class TestKillRing(unittest.TestCase):
@classmethod
def setUpClass(cls):
""" Create the application for the test case.
"""
cls._app = QtWidgets.QApplication.instance()
if cls._app is None:
cls._app = QtWidgets.QApplication([])
cls._app.setQuitOnLastWindowClosed(False)
@classmethod
def tearDownClass(cls):
""" Exit the application.
"""
QtWidgets.QApplication.quit()
def test_generic(self):
""" Does the generic kill ring work?
"""
ring = KillRing()
self.assertTrue(ring.yank() is None)
self.assertTrue(ring.rotate() is None)
ring.kill('foo')
self.assertEqual(ring.yank(), 'foo')
self.assertTrue(ring.rotate() is None)
self.assertEqual(ring.yank(), 'foo')
ring.kill('bar')
self.assertEqual(ring.yank(), 'bar')
self.assertEqual(ring.rotate(), 'foo')
ring.clear()
self.assertTrue(ring.yank() is None)
self.assertTrue(ring.rotate() is None)
def test_qt_basic(self):
""" Does the Qt kill ring work?
"""
text_edit = QtWidgets.QPlainTextEdit()
ring = QtKillRing(text_edit)
ring.kill('foo')
ring.kill('bar')
ring.yank()
ring.rotate()
ring.yank()
self.assertEqual(text_edit.toPlainText(), 'foobar')
text_edit.clear()
ring.kill('baz')
ring.yank()
ring.rotate()
ring.rotate()
ring.rotate()
self.assertEqual(text_edit.toPlainText(), 'foo')
def test_qt_cursor(self):
""" Does the Qt kill ring maintain state with cursor movement?
"""
text_edit = QtWidgets.QPlainTextEdit()
ring = QtKillRing(text_edit)
ring.kill('foo')
ring.kill('bar')
ring.yank()
text_edit.moveCursor(QtGui.QTextCursor.Left)
ring.rotate()
self.assertEqual(text_edit.toPlainText(), 'bar')
if __name__ == '__main__':
import pytest
pytest.main()

View file

@ -0,0 +1,15 @@
import unittest
from qtconsole.styles import dark_color, dark_style
class TestStyles(unittest.TestCase):
def test_dark_color(self):
self.assertTrue(dark_color('#000000')) # black
self.assertTrue(not dark_color('#ffff66')) # bright yellow
self.assertTrue(dark_color('#80807f')) # < 50% gray
self.assertTrue(not dark_color('#808080')) # = 50% gray
def test_dark_style(self):
self.assertTrue(dark_style('monokai'))
self.assertTrue(not dark_style('default'))

View file

@ -0,0 +1,194 @@
"""
Usage information for QtConsole
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
gui_reference = """\
=====================
The Jupyter QtConsole
=====================
This console is designed to emulate the look, feel and workflow of a terminal
environment. Beyond this basic design, the console also implements
functionality not currently found in most terminal emulators. Some examples of
these console enhancements are inline syntax highlighting, multiline editing,
inline graphics, and others.
This quick reference document contains the basic information you'll need to
know to make the most efficient use of it. For the various command line
options available at startup, type ``jupyter qtconsole --help`` at the command
line.
Multiline editing
=================
The graphical console is capable of true multiline editing, but it also tries
to behave intuitively like a terminal when possible. If you are used to
IPython's old terminal behavior, you should find the transition painless. If
you learn to use a few basic keybindings, the console provides even greater
efficiency.
For single expressions or indented blocks, the console behaves almost like the
IPython terminal: single expressions are immediately evaluated, and indented
blocks are evaluated once a single blank line is entered::
In [1]: print ("Hello Jupyter!") # Enter was pressed at the end of the line
Hello Jupyter!
In [2]: for num in range(10):
...: print(num)
...:
0 1 2 3 4 5 6 7 8 9
If you want to enter more than one expression in a single input block
(something not possible in the terminal), you can use ``Control-Enter`` at the
end of your first line instead of ``Enter``. At that point the console goes
into 'cell mode' and even if your inputs are not indented, it will continue
accepting lines until either you enter an extra blank line or
you hit ``Shift-Enter`` (the key binding that forces execution). When a
multiline cell is entered, the console analyzes it and executes its code producing
an ``Out[n]`` prompt only for the last expression in it, while the rest of the
cell is executed as if it was a script. An example should clarify this::
In [3]: x=1 # Hit Ctrl-Enter here
...: y=2 # from now on, regular Enter is sufficient
...: z=3
...: x**2 # This does *not* produce an Out[] value
...: x+y+z # Only the last expression does
...:
Out[3]: 6
The behavior where an extra blank line forces execution is only active if you
are actually typing at the keyboard each line, and is meant to make it mimic
the IPython terminal behavior. If you paste a long chunk of input (for example
a long script copied form an editor or web browser), it can contain arbitrarily
many intermediate blank lines and they won't cause any problems. As always,
you can then make it execute by appending a blank line *at the end* or hitting
``Shift-Enter`` anywhere within the cell.
With the up arrow key, you can retrieve previous blocks of input that contain
multiple lines. You can move inside of a multiline cell like you would in any
text editor. When you want it executed, the simplest thing to do is to hit the
force execution key, ``Shift-Enter`` (though you can also navigate to the end
and append a blank line by using ``Enter`` twice).
If you are editing a multiline cell and accidentally navigate out of it using the
up or down arrow keys, the console clears the cell and replaces it with the
contents of the cell which the up or down arrow key stopped on. If you wish to
to undo this action, perhaps because of an accidental keypress, use the Undo
keybinding, ``Control-z``, to restore the original cell.
Key bindings
============
The Jupyter QtConsole supports most of the basic Emacs line-oriented keybindings,
in addition to some of its own.
The keybindings themselves are:
- ``Enter``: insert new line (may cause execution, see above).
- ``Ctrl-Enter``: *force* new line, *never* causes execution.
- ``Shift-Enter``: *force* execution regardless of where cursor is, no newline added.
- ``Up``: step backwards through the history.
- ``Down``: step forwards through the history.
- ``Shift-Up``: search backwards through the history (like ``Control-r`` in bash).
- ``Shift-Down``: search forwards through the history.
- ``Control-c``: copy highlighted text to clipboard (prompts are automatically stripped).
- ``Control-Shift-c``: copy highlighted text to clipboard (prompts are not stripped).
- ``Control-v``: paste text from clipboard.
- ``Control-z``: undo (retrieves lost text if you move out of a cell with the arrows).
- ``Control-Shift-z``: redo.
- ``Control-o``: move to 'other' area, between pager and terminal.
- ``Control-l``: clear terminal.
- ``Control-a``: go to beginning of line.
- ``Control-e``: go to end of line.
- ``Control-u``: kill from cursor to the begining of the line.
- ``Control-k``: kill from cursor to the end of the line.
- ``Control-y``: yank (paste)
- ``Control-p``: previous line (like up arrow)
- ``Control-n``: next line (like down arrow)
- ``Control-f``: forward (like right arrow)
- ``Control-b``: back (like left arrow)
- ``Control-d``: delete next character, or exits if input is empty
- ``Alt-<``: move to the beginning of the input region.
- ``alt->``: move to the end of the input region.
- ``Alt-d``: delete next word.
- ``Alt-Backspace``: delete previous word.
- ``Control-.``: force a kernel restart (a confirmation dialog appears).
- ``Control-+``: increase font size.
- ``Control--``: decrease font size.
- ``Control-Alt-Space``: toggle full screen. (Command-Control-Space on Mac OS X)
The pager
=========
The Jupyter QtConsole will show long blocks of text from many sources using a
built-in pager. You can control where this pager appears with the ``--paging``
command-line flag:
- ``inside`` [default]: the pager is overlaid on top of the main terminal. You
must quit the pager to get back to the terminal (similar to how a pager such
as ``less`` or ``more`` pagers behave).
- ``vsplit``: the console is made double height, and the pager appears on the
bottom area when needed. You can view its contents while using the terminal.
- ``hsplit``: the console is made double width, and the pager appears on the
right area when needed. You can view its contents while using the terminal.
- ``none``: the console displays output without paging.
If you use the vertical or horizontal paging modes, you can navigate between
terminal and pager as follows:
- Tab key: goes from pager to terminal (but not the other way around).
- Control-o: goes from one to another always.
- Mouse: click on either.
In all cases, the ``q`` or ``Escape`` keys quit the pager (when used with the
focus on the pager area).
Running subprocesses
====================
When running a subprocess from the kernel, you can not interact with it as if
it was running in a terminal. So anything that invokes a pager or expects
you to type input into it will block and hang (you can kill it with ``Control-C``).
The console can use magics provided by the IPython kernel. These magics include
``%less`` to page files (aliased to ``%more``),
``%clear`` to clear the terminal, and ``%man`` on Linux/OSX. These cover the
most common commands you'd want to call in your subshell and that would cause
problems if invoked via ``!cmd``, but you need to be aware of this limitation.
Display
=======
For example, if using the IPython kernel, there are functions available for
object display:
In [4]: from IPython.display import display
In [5]: from IPython.display import display_png, display_svg
Python objects can simply be passed to these functions and the appropriate
representations will be displayed in the console as long as the objects know
how to compute those representations. The easiest way of teaching objects how
to format themselves in various representations is to define special methods
such as: ``_repr_svg_`` and ``_repr_png_``. IPython's display formatters
can also be given custom formatter functions for various types::
In [6]: ip = get_ipython()
In [7]: png_formatter = ip.display_formatter.formatters['image/png']
In [8]: png_formatter.for_type(Foo, foo_to_png)
For further details, see ``IPython.core.formatters``.
"""

View file

@ -0,0 +1,109 @@
""" Defines miscellaneous Qt-related helper classes and functions.
"""
import inspect
from qtpy import QtCore, QtGui
from ipython_genutils.py3compat import iteritems
from traitlets import HasTraits, TraitType
#-----------------------------------------------------------------------------
# Metaclasses
#-----------------------------------------------------------------------------
MetaHasTraits = type(HasTraits)
MetaQObject = type(QtCore.QObject)
class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):
""" A metaclass that inherits from the metaclasses of HasTraits and QObject.
Using this metaclass allows a class to inherit from both HasTraits and
QObject. Using SuperQObject instead of QObject is highly recommended. See
QtKernelManager for an example.
"""
def __new__(mcls, name, bases, classdict):
# FIXME: this duplicates the code from MetaHasTraits.
# I don't think a super() call will help me here.
for k,v in iteritems(classdict):
if isinstance(v, TraitType):
v.name = k
elif inspect.isclass(v):
if issubclass(v, TraitType):
vinst = v()
vinst.name = k
classdict[k] = vinst
cls = MetaQObject.__new__(mcls, name, bases, classdict)
return cls
def __init__(mcls, name, bases, classdict):
# Note: super() did not work, so we explicitly call these.
MetaQObject.__init__(mcls, name, bases, classdict)
MetaHasTraits.__init__(mcls, name, bases, classdict)
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
def superQ(QClass):
""" Permits the use of super() in class hierarchies that contain Qt classes.
Unlike QObject, SuperQObject does not accept a QObject parent. If it did,
super could not be emulated properly (all other classes in the heierarchy
would have to accept the parent argument--they don't, of course, because
they don't inherit QObject.)
This class is primarily useful for attaching signals to existing non-Qt
classes. See QtKernelManagerMixin for an example.
"""
class SuperQClass(QClass):
def __new__(cls, *args, **kw):
# We initialize QClass as early as possible. Without this, Qt complains
# if SuperQClass is not the first class in the super class list.
inst = QClass.__new__(cls)
QClass.__init__(inst)
return inst
def __init__(self, *args, **kw):
# Emulate super by calling the next method in the MRO, if there is one.
mro = self.__class__.mro()
for qt_class in QClass.mro():
mro.remove(qt_class)
next_index = mro.index(SuperQClass) + 1
if next_index < len(mro):
mro[next_index].__init__(self, *args, **kw)
return SuperQClass
SuperQObject = superQ(QtCore.QObject)
#-----------------------------------------------------------------------------
# Functions
#-----------------------------------------------------------------------------
def get_font(family, fallback=None):
"""Return a font of the requested family, using fallback as alternative.
If a fallback is provided, it is used in case the requested family isn't
found. If no fallback is given, no alternative is chosen and Qt's internal
algorithms may automatically choose a fallback font.
Parameters
----------
family : str
A font name.
fallback : str
A font name.
Returns
-------
font : QFont object
"""
font = QtGui.QFont(family)
# Check whether we got what we wanted using QFontInfo, since exactMatch()
# is overly strict and returns false in too many cases.
font_info = QtGui.QFontInfo(font)
if fallback is not None and font_info.family() != family:
font = QtGui.QFont(fallback)
return font