34

I'd like to embed an IPython qt console widget in a PyQt application I am working on. The code provided below (and adapted from https://stackoverflow.com/a/9796491/1332492) Accomplishes this for IPython v0.12. However, this crashes in IPython v0.13 at the line self.heartbeat.start() with RuntimeError: threads can only be started once. Commenting out this line brings up the widget, but doesn't respond to user input.

Does anyone know how to achieve the equivalent functionality for IPython v0.13?

"""
Adapted from
https://stackoverflow.com/a/9796491/1332492
"""
import os
import atexit

from IPython.zmq.ipkernel import IPKernelApp
from IPython.lib.kernel import find_connection_file
from IPython.frontend.qt.kernelmanager import QtKernelManager
from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
from IPython.config.application import catch_config_error
from PyQt4 import QtCore


class IPythonLocalKernelApp(IPKernelApp):
    DEFAULT_INSTANCE_ARGS = ['']

    @catch_config_error
    def initialize(self, argv=None):
        super(IPythonLocalKernelApp, self).initialize(argv)
        self.kernel.eventloop = self.loop_qt4_nonblocking

    def loop_qt4_nonblocking(self, kernel):
        """Non-blocking version of the ipython qt4 kernel loop"""
        kernel.timer = QtCore.QTimer()
        kernel.timer.timeout.connect(kernel.do_one_iteration)
        kernel.timer.start(1000*kernel._poll_interval)

    def start(self, argv=DEFAULT_INSTANCE_ARGS):
        """Starts IPython kernel app
        argv: arguments passed to kernel
        """
        self.initialize(argv)
        self.heartbeat.start()

        if self.poller is not None:
            self.poller.start()

        self.kernel.start()


class IPythonConsoleQtWidget(RichIPythonWidget):
    _connection_file = None

    def __init__(self, *args, **kw):
        RichIPythonWidget.__init__(self, *args, **kw)
        self._existing = True
        self._may_close = False
        self._confirm_exit = False

    def _init_kernel_manager(self):
        km = QtKernelManager(connection_file=self._connection_file, config=self.config)
        km.load_connection_file()
        km.start_channels(hb=self._heartbeat)
        self.kernel_manager = km
        atexit.register(self.kernel_manager.cleanup_connection_file)

    def connect_kernel(self, connection_file, heartbeat=False):
        self._heartbeat = heartbeat
        if os.path.exists(connection_file):
            self._connection_file = connection_file
        else:
            self._connection_file = find_connection_file(connection_file)

        self._init_kernel_manager()


def main(**kwargs):
    kernelapp = IPythonLocalKernelApp.instance()
    kernelapp.start()

    widget = IPythonConsoleQtWidget()
    widget.connect_kernel(connection_file=kernelapp.connection_file)
    widget.show()

    return widget

if __name__ == "__main__":
    from PyQt4.QtGui import QApplication
    app = QApplication([''])
    main()
    app.exec_()

Traceback for v0.13

RuntimeError                              Traceback (most recent call last)
/Users/beaumont/terminal.py in <module>()
     80     from PyQt4.QtGui import QApplication
     81     app = QApplication([''])
---> 82     main()
        global main = <function main at 0x106d0c848>
     83     app.exec_()

/Users/beaumont/terminal.py in main(**kwargs={})
     69 def main(**kwargs):
     70     kernelapp = IPythonLocalKernelApp.instance()
---> 71     kernelapp.start()
        kernelapp.start = <bound method IPythonLocalKernelApp.start of     <__main__.IPythonLocalKernelApp object at 0x106d10590>>
     72 
     73     widget = IPythonConsoleQtWidget()

/Users/beaumont/terminal.py in start(self=<__main__.IPythonLocalKernelApp object>, argv=[''])
     33         """
     34         self.initialize(argv)
---> 35         self.heartbeat.start()
        self.heartbeat.start = <bound method Heartbeat.start of <Heartbeat(Thread-1, started daemon 4458577920)>>
     36 
     37         if self.poller is not None:

/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.pyc in start(self=<Heartbeat(Thread-1, started daemon 4458577920)>)
    487             raise RuntimeError("thread.__init__() not called")
    488         if self.__started.is_set():
--> 489             raise RuntimeError("threads can only be started once")
        global RuntimeError = undefined
    490         if __debug__:
    491             self._note("%s.start(): starting thread", self)

RuntimeError: threads can only be started once
Community
  • 1
  • 1
ChrisB
  • 4,628
  • 7
  • 29
  • 41

6 Answers6

15

Ok, this code seems to do the trick (i.e. it puts a non-blocking ipython interpreter in a Qt widget, which can be embedded into other widgets). Keywords passed to terminal_widget get added to the namespace of the widget

import atexit

from IPython.zmq.ipkernel import IPKernelApp
from IPython.lib.kernel import find_connection_file
from IPython.frontend.qt.kernelmanager import QtKernelManager
from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
from IPython.utils.traitlets import TraitError
from PyQt4 import QtGui, QtCore

def event_loop(kernel):
    kernel.timer = QtCore.QTimer()
    kernel.timer.timeout.connect(kernel.do_one_iteration)
    kernel.timer.start(1000*kernel._poll_interval)

def default_kernel_app():
    app = IPKernelApp.instance()
    app.initialize(['python', '--pylab=qt'])
    app.kernel.eventloop = event_loop
    return app

def default_manager(kernel):
    connection_file = find_connection_file(kernel.connection_file)
    manager = QtKernelManager(connection_file=connection_file)
    manager.load_connection_file()
    manager.start_channels()
    atexit.register(manager.cleanup_connection_file)
    return manager

def console_widget(manager):
    try: # Ipython v0.13
        widget = RichIPythonWidget(gui_completion='droplist')
    except TraitError:  # IPython v0.12
        widget = RichIPythonWidget(gui_completion=True)
    widget.kernel_manager = manager
    return widget

def terminal_widget(**kwargs):
    kernel_app = default_kernel_app()
    manager = default_manager(kernel_app)
    widget = console_widget(manager)

    #update namespace                                                           
    kernel_app.shell.user_ns.update(kwargs)

    kernel_app.start()
    return widget

app = QtGui.QApplication([])
widget = terminal_widget(testing=123)
widget.show()
app.exec_()
flying sheep
  • 8,475
  • 5
  • 56
  • 73
ChrisB
  • 4,628
  • 7
  • 29
  • 41
  • 5
    Hi Chris, this is something we've wanted to make possible for a while. Would you like to integrate something like this into IPython? If you're interested, come and discuss it on the issue: https://github.com/ipython/ipython/issues/1085 – Thomas K Aug 16 '12 at 12:13
  • unfortunately, this breaks the `input()` builtin, which is e.g. needed for `help()` (without arguments). i don’t know why, but calling `input()` directly or indirectly simply freezes the application. – flying sheep Feb 18 '13 at 18:01
  • 4
    Just in case anyone else wonders what happened to this work, here is the successful pull request it finally culminated in: https://github.com/ipython/ipython/pull/2724 and here's a gist demonstrating its use: https://gist.github.com/pberkes/5266744 – Charl Botha May 01 '13 at 09:05
  • (PR #2724 has NOT been backported to the 0.13.x branch of IPython as far as I can see.) – Charl Botha May 01 '13 at 09:19
15

The accepted answer by @ChrisB is fine for IPython version 0.13, but it doesn't work with newer versions. From the examples section of the IPython kernel repository on github, this is the way to do it in v1.x+ (currently tested with 4.0.1), which has the feature that the console and kernel are in the same process.

Here is an example, based on the official one, which gives a convenience class that can be easily plugged into an application. It's setup to work with pyqt4 and IPython 4.0.1 on Python 2.7:

(Note: you'll need to install the ipykernel and qtconsole packages)

# Set the QT API to PyQt4
import os
os.environ['QT_API'] = 'pyqt'
import sip
sip.setapi("QString", 2)
sip.setapi("QVariant", 2)
from PyQt4.QtGui  import *
# Import the console machinery from ipython
from qtconsole.rich_ipython_widget import RichIPythonWidget
from qtconsole.inprocess import QtInProcessKernelManager
from IPython.lib import guisupport

class QIPythonWidget(RichIPythonWidget):
    """ Convenience class for a live IPython console widget. We can replace the standard banner using the customBanner argument"""
    def __init__(self,customBanner=None,*args,**kwargs):
        if not customBanner is None: self.banner=customBanner
        super(QIPythonWidget, self).__init__(*args,**kwargs)
        self.kernel_manager = kernel_manager = QtInProcessKernelManager()
        kernel_manager.start_kernel()
        kernel_manager.kernel.gui = 'qt4'
        self.kernel_client = kernel_client = self._kernel_manager.client()
        kernel_client.start_channels()

        def stop():
            kernel_client.stop_channels()
            kernel_manager.shutdown_kernel()
            guisupport.get_app_qt4().exit()            
        self.exit_requested.connect(stop)

    def pushVariables(self,variableDict):
        """ Given a dictionary containing name / value pairs, push those variables to the IPython console widget """
        self.kernel_manager.kernel.shell.push(variableDict)
    def clearTerminal(self):
        """ Clears the terminal """
        self._control.clear()    
    def printText(self,text):
        """ Prints some plain text to the console """
        self._append_plain_text(text)        
    def executeCommand(self,command):
        """ Execute a command in the frame of the console widget """
        self._execute(command,False)


class ExampleWidget(QWidget):
    """ Main GUI Widget including a button and IPython Console widget inside vertical layout """
    def __init__(self, parent=None):
        super(ExampleWidget, self).__init__(parent)
        layout = QVBoxLayout(self)
        self.button = QPushButton('Another widget')
        ipyConsole = QIPythonWidget(customBanner="Welcome to the embedded ipython console\n")
        layout.addWidget(self.button)
        layout.addWidget(ipyConsole)        
        # This allows the variable foo and method print_process_id to be accessed from the ipython console
        ipyConsole.pushVariables({"foo":43,"print_process_id":print_process_id})
        ipyConsole.printText("The variable 'foo' and the method 'print_process_id()' are available. Use the 'whos' command for information.")                           

def print_process_id():
    print 'Process ID is:', os.getpid()        

def main():
    app  = QApplication([])
    widget = ExampleWidget()
    widget.show()
    app.exec_()    

if __name__ == '__main__':
    main()
Tim Rae
  • 3,167
  • 2
  • 28
  • 35
13

A 2016 update working in PyQt5:

from qtpy import QtGui
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtconsole.inprocess import QtInProcessKernelManager
from IPython.lib import guisupport


class ConsoleWidget(RichJupyterWidget):

    def __init__(self, customBanner=None, *args, **kwargs):
        super(ConsoleWidget, self).__init__(*args, **kwargs)

        if customBanner is not None:
            self.banner = customBanner

        self.font_size = 6
        self.kernel_manager = kernel_manager = QtInProcessKernelManager()
        kernel_manager.start_kernel(show_banner=False)
        kernel_manager.kernel.gui = 'qt'
        self.kernel_client = kernel_client = self._kernel_manager.client()
        kernel_client.start_channels()

        def stop():
            kernel_client.stop_channels()
            kernel_manager.shutdown_kernel()
            guisupport.get_app_qt().exit()

        self.exit_requested.connect(stop)

    def push_vars(self, variableDict):
        """
        Given a dictionary containing name / value pairs, push those variables
        to the Jupyter console widget
        """
        self.kernel_manager.kernel.shell.push(variableDict)

    def clear(self):
        """
        Clears the terminal
        """
        self._control.clear()

        # self.kernel_manager

    def print_text(self, text):
        """
        Prints some plain text to the console
        """
        self._append_plain_text(text)

    def execute_command(self, command):
        """
        Execute a command in the frame of the console widget
        """
        self._execute(command, False)


if __name__ == '__main__':
    app = QtGui.QApplication([])
    widget = ConsoleWidget()
    widget.show()
    app.exec_()
Nuno André
  • 4,739
  • 1
  • 33
  • 46
Santi Peñate-Vera
  • 1,053
  • 4
  • 33
  • 68
  • Works great with Qt5 thanks. One note - if one cares to get rid off all the debug logging that comes in add kernel_manager.kernel.log.setLevel(logging.CRITICAL) before the start_kernel call – Techniquab Oct 17 '17 at 19:52
  • 1
    How can it be done that the console widget is non-blocking? If I execute some command that takes time, the whole Qt GUI is blocked. – waszil Jul 05 '18 at 07:00
  • 1
    You can use QThread or QRunnable. That is a typical case of GUI's and the solution is to use threads – Santi Peñate-Vera Jul 05 '18 at 07:54
  • 2
    @SantiPeñate-Vera I use threads and runnables for other things, but what exactly should I put into a thread fr example with RichJupiterWidget? As far as I understand, IPython and this widget has its own thread/process/whatever, and handles this stuff in its kernel. Also, there is this [post](https://stackoverflow.com/questions/11513132/embedding-ipython-qt-console-in-a-pyqt-application/12375397#12375397), that suggests that it can be handled somehow (kernel.eventloop) (does not work for me) – waszil Jul 05 '18 at 10:26
  • Is there any reason that the console widget should not load at first while it does load on restart? Such as in an application when you later open the console v applications where the console is open from the beginning? – Philip09 Apr 10 '23 at 15:27
4

IPython 0.13 version with some cleanups:

#coding: utf-8
'''
Updated for IPython 0.13
Created on 18-03-2012
Updated:   11-09-2012
@author: Paweł Jarosz
'''

import atexit

from PySide import QtCore, QtGui

from IPython.zmq.ipkernel import IPKernelApp
from IPython.lib.kernel import find_connection_file
from IPython.frontend.qt.kernelmanager import QtKernelManager
from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
from IPython.config.application import catch_config_error

DEFAULT_INSTANCE_ARGS = ['qtconsole','--pylab=inline', '--colors=linux']

class IPythonLocalKernelApp(IPKernelApp):
    @catch_config_error
    def initialize(self, argv=DEFAULT_INSTANCE_ARGS):
        """
        argv: IPython args

        example:

            app = QtGui.QApplication([])
            kernelapp = IPythonLocalKernelApp.instance()
            kernelapp.initialize()

            widget = IPythonConsoleQtWidget()
            widget.set_default_style(colors='linux')

            widget.connect_kernel(connection_file=kernelapp.get_connection_file())
            # if you won't to connect to remote kernel you don't need kernelapp part, just widget part and:

            # widget.connect_kernel(connection_file='kernel-16098.json')

            # where kernel-16098.json is the kernel name
            widget.show()

            namespace = kernelapp.get_user_namespace()
            nxxx = 12
            namespace["widget"] = widget
            namespace["QtGui"]=QtGui
            namespace["nxxx"]=nxxx

            app.exec_()
        """
        super(IPythonLocalKernelApp, self).initialize(argv)
        self.kernel.eventloop = self.loop_qt4_nonblocking
        self.kernel.start()
        self.start()

    def loop_qt4_nonblocking(self, kernel):
        """Non-blocking version of the ipython qt4 kernel loop"""
        kernel.timer = QtCore.QTimer()
        kernel.timer.timeout.connect(kernel.do_one_iteration)
        kernel.timer.start(1000*kernel._poll_interval)

    def get_connection_file(self):
        """Returne current kernel connection file."""
        return self.connection_file

    def get_user_namespace(self):
        """Returns current kernel userspace dict"""
        return self.kernel.shell.user_ns

class IPythonConsoleQtWidget(RichIPythonWidget):

    def connect_kernel(self, connection_file, heartbeat = False):
        """
        connection_file: str - is the connection file name, for example 'kernel-16098.json'
        heartbeat: bool - workaround, needed for right click/save as ... errors ... i don't know how to 
                          fix this issue. Anyone knows? Anyway it needs more testing
            example1 (standalone):

                    app = QtGui.QApplication([])
                    widget = IPythonConsoleQtWidget()
                    widget.set_default_style(colors='linux')


                    widget.connect_kernel(connection_file='some connection file name')

                    app.exec_()

            example2 (IPythonLocalKernelApp):

                    app = QtGui.QApplication([])

                    kernelapp = IPythonLocalKernelApp.instance()
                    kernelapp.initialize()

                    widget = IPythonConsoleQtWidget()

                    # Green text, black background ;)
                    widget.set_default_style(colors='linux')

                    widget.connect_kernel(connection_file='kernelapp.get_connection_file())

                    app.exec_()

        """
        km = QtKernelManager(connection_file=find_connection_file(connection_file), config=self.config)
        km.load_connection_file()
        km.start_channels(hb=heartbeat)
        self.kernel_manager = km
        atexit.register(self.kernel_manager.cleanup_connection_file)

def main():

    app = QtGui.QApplication([])
    kernelapp = IPythonLocalKernelApp.instance()
    kernelapp.initialize()

    widget = IPythonConsoleQtWidget()
    widget.set_default_style(colors='linux')

    widget.connect_kernel(connection_file=kernelapp.get_connection_file())
    # if you connect to outside app kernel you don't need kernelapp part, 
    # just widget part and:

    # widget.connect_kernel(connection_file='kernel-16098.json')

    # where kernel-16098.json is the kernel name
    widget.show()

    namespace = kernelapp.get_user_namespace()
    nxxx = 12
    namespace["widget"] = widget
    namespace["QtGui"]=QtGui
    namespace["nxxx"]=nxxx

    app.exec_()


if __name__=='__main__':
    main()    
Paweł Jarosz
  • 581
  • 4
  • 5
  • as with the answer you based this on: unfortunately this breaks upon calling `input()` (which is for example used in the `help()` repl). the applications simply freezes if `input()` is called. – flying sheep Feb 18 '13 at 18:04
2

Possibly helping others researching this: I came across this example:

https://github.com/gpoulin/python-test/blob/master/embedded_qtconsole.py

Tested and works with PySide, IPython 2.1.0, Python 3.4.1. It appears I can even use matplotlib directly.

from IPython.qt.console.rich_ipython_widget import RichIPythonWidget
from IPython.qt.inprocess import QtInProcessKernelManager
from PySide import QtGui, QtCore


class EmbedIPython(RichIPythonWidget):

    def __init__(self, **kwarg):
        super(RichIPythonWidget, self).__init__()
        self.kernel_manager = QtInProcessKernelManager()
        self.kernel_manager.start_kernel()
        self.kernel = self.kernel_manager.kernel
        self.kernel.gui = 'qt4'
        self.kernel.shell.push(kwarg)
        self.kernel_client = self.kernel_manager.client()
        self.kernel_client.start_channels()


class MainWindow(QtGui.QMainWindow):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.textEdit = QtGui.QTextEdit()

        but1 = QtGui.QPushButton('write')
        but1.clicked.connect(self.but_write)

        but2 = QtGui.QPushButton('read')
        but2.clicked.connect(self.but_read)

        self.a = {'text': ''}
        self.console = EmbedIPython(testing=123, a=self.a)
        self.console.kernel.shell.run_cell('%pylab qt')

        vbox = QtGui.QVBoxLayout()
        hbox = QtGui.QHBoxLayout()
        vbox.addWidget(self.textEdit)
        vbox.addWidget(self.console)
        hbox.addWidget(but1)
        hbox.addWidget(but2)
        vbox.addLayout(hbox)

        b = QtGui.QWidget()
        b.setLayout(vbox)
        self.setCentralWidget(b)

    def but_read(self):
        self.a['text'] = self.textEdit.toPlainText()
        self.console.execute("print('a[\\\'text\\\'] = \"'+ a['text'] +'\"')")

    def but_write(self):
        self.textEdit.setText(self.a['text'])


if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    main = MainWindow()
    main.show()
    sys.exit(app.exec_())
Åsmund Hj
  • 151
  • 4
  • Thanks for posting your answer! Please note that you should post the essential parts of the answer here, on this site, or your post risks being deleted [See the FAQ where it mentions answers that are 'barely more than a link'.](http://stackoverflow.com/faq#deletion) You may still include the link if you wish, but only as a 'reference'. The answer should stand on its own without needing the link. – Taryn Oct 06 '14 at 11:18
1

Updated answer for:

  • Python 3.8
  • IPython 7.22.0
  • QtConsole 5.0.3

Modified from previous answer.

from qtpy.QtWidgets import QApplication
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtconsole.inprocess import QtInProcessKernelManager
from IPython.lib import guisupport


class ConsoleWidget(RichJupyterWidget):

    def __init__(self, customBanner=None, *args, **kwargs):
        super(ConsoleWidget, self).__init__(*args, **kwargs)

        if customBanner is not None:
            self.banner = customBanner

        self.font_size = 10
        self.kernel_manager = QtInProcessKernelManager()
        self.kernel_manager.start_kernel(show_banner=False)
        self.kernel_manager.kernel.gui = 'qt'
        self.kernel_client = self._kernel_manager.client()
        self.kernel_client.start_channels()

        def stop():
            self.kernel_client.stop_channels()
            self.kernel_manager.shutdown_kernel()
            guisupport.get_app_qt4().exit()

        self.exit_requested.connect(stop)

    def push_vars(self, variableDict):
        """
        Given a dictionary containing name / value pairs, push those variables
        to the Jupyter console widget
        """
        self.kernel_manager.kernel.shell.push(variableDict)

    def clear(self):
        """
        Clears the terminal
        """
        self._control.clear()

    def print_text(self, text):
        """
        Prints some plain text to the console
        """
        self._append_plain_text(text)

    def execute_command(self, command):
        """
        Execute a command in the frame of the console widget
        """
        self._execute(command, False)


if __name__ == '__main__':
    app = QApplication([])
    widget = ConsoleWidget()
    widget.show()
    app.exec_()
JS00N
  • 91
  • 1
  • 4