diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 439c6ce..57693c8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,69 +1,55 @@ name: CI + on: - workflow_dispatch: push: branches: - main - - master pull_request: branches: - main - - master jobs: unix: - runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-20.04, ubuntu-22.04, macos-11, macos-12] - + + defaults: + run: + shell: bash -l {0} steps: - - uses: actions/checkout@v2 - - name: Get number of CPU cores - uses: SimenB/github-actions-cpu-cores@v1 + - name: Checkout repository + uses: actions/checkout@v4 - - name: install mamba - uses: mamba-org/provision-with-micromamba@main + - name: Setup conda environment + uses: mamba-org/setup-micromamba@v1 with: environment-file: env.yml - environment-name: xeusqt - - name: install cxx compiler - shell: bash -l {0} - run: | - $HOME/micromamba-bin/micromamba install cxx-compiler libuuid -c conda-forge -y - - - name: cmake configure - shell: bash -l {0} + - name: Configure using CMake run: | mkdir -p build cd build cmake .. \ -DXEUS_BUILD_TESTS=ON \ -DCMAKE_PREFIX_PATH=$CONDA_PREFIX \ - -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX + -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX - - name: build - shell: bash -l {0} + - name: Build run: | cd build - make -j 2 - # make -j ${{ steps.cpu-cores.outputs.count }} + make - - - name: install xeus-qt - shell: bash -l {0} + - name: Install run: | cd build make install - win: - runs-on: ${{ matrix.os }} strategy: @@ -71,27 +57,25 @@ jobs: matrix: os: [ windows-2019, windows-2022 ] + defaults: + run: + shell: cmd /C call {0} + steps: - - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v4 - - name: install mamba - uses: mamba-org/provision-with-micromamba@main + - name: Setup conda environment + uses: mamba-org/setup-micromamba@v1 with: environment-file: env.yml - environment-name: xeus - - - name: micromamba shell hook - shell: powershell - run: | - micromamba shell hook -s cmd.exe -p C:\Users\runneradmin\micromamba-root + init-shell: cmd.exe - name: Make build directory run: mkdir build - - name: cmake configure - shell: cmd + - name: Configure using CMake run: | - call C:\Users\runneradmin\micromamba-root\condabin\micromamba.bat activate xeus cmake .. ^ -G Ninja ^ -DCMAKE_BUILD_TYPE=Release ^ @@ -101,10 +85,7 @@ jobs: -DCMAKE_INSTALL_PREFIX="%CONDA_PREFIX%" working-directory: build - - name: build - shell: cmd + - name: Build and install run: | - call C:\Users\runneradmin\micromamba-root\condabin\micromamba.bat activate xeus - set CL=/MP ninja install working-directory: build diff --git a/README.md b/README.md index 7725b2d..8d6d491 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,32 @@ [![CI](https://github.com/DerThorsten/xeus-qt-python/actions/workflows/main.yml/badge.svg)](https://github.com/DerThorsten/xeus-qt-python/actions/workflows/main.yml) +You will need to compile [xeus-qt](https://github.com/jupyter-xeus/xeus-qt) and [xeus-qt-python](https://github.com/jupyter-xeus/xeus-qt-python) (this repository) from source. +First clone these two repositories under `path/to/xeus-qt` and `path/to/xeus-qt-python`, respectively (or whatever place you'd like). +Then enter in a terminal: + +```console +micromamba create -f env.yml +micromamba activate xeus-qt-python +cd path/to/xeus-qt +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX +make install +cd path/to/xeus-qt-python +mkdir build +cd build +cmake .. -DCMAKE_PREFIX_PATH=$CONDA_PREFIX -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX +make +make install +cd .. +python kernel_widget.py +``` + +This launches JupyterLab in a Qt application. If you look at running kernels in the left tab, you will see a kernel named `qt-python`. +Right-click on it and choose "New Console for Kernel". Now go to the file browser and double-click on the `my_notebook.ipynb` notebook. +In the "Select Kernel" menu, under "Use Kernel from Other Session" at the bottom, select "Console 1". +Now execute all the cells. The last one should display a button with the name "black magic" at the bottom of the Qt application. +If you click on it, it should display "hello from here" inside the last cell. +That's it, you've just connected your custom Qt application to Jupyter! +You added a button to your Qt application from a Jupyter notebook, which when clicked on, prints a message to a notebook cell output. diff --git a/env.yml b/env.yml index 1dbd14b..37a0148 100644 --- a/env.yml +++ b/env.yml @@ -1,45 +1,31 @@ -name: base +name: xeus-qt-python channels: - conda-forge dependencies: + - compilers - cmake - - pkg-config - - zeromq - - cppzmq + - make # [not win] + - ninja # [win] + - python + - pip + - qt + - xeus + - xeus-qt >=0.1.3 + - xeus-zmq + - xeus-python - xtl - - OpenSSL - nlohmann_json - - xeus >=3.0.0,<4 - - xeus-qt >=0.1.2,<0.2 - - xeus-python + - cppzmq - pybind11 - pybind11_json - - xeus >= 3.0.3 - - xeus-zmq >= 1.0.2 - - python=3.10 - - fps-jupyterlab - - fps-auth - - python >=3.7 - - fastapi >=0.87.0,<1 - - fps >=0.0.21,<1 - - fps-uvicorn >=0.0.19,<1 - - fps-auth-base <1 - - fps-contents >=0.0.37,<1 - - fps-nbconvert >=0.0.37,<1 - - fps-yjs >=0.0.37,<1 - pyqt - - PyQtWebEngine - qtpy - - hatch - - git - - make - - cxx-compiler - - mesa-libgl-cos7-aarch64 - - mesa-libgl-cos6-x86_64 - - mesa-dri-drivers-cos6-x86_64 - - libselinux-cos6-x86_64 - - libxdamage-cos6-x86_64 - - libxxf86vm-cos6-x86_64 - - libxext-cos6-x86_64 - - xorg-libxfixes + - pyqtwebengine + - pyside2 + - fps-lab + - fps-auth + - fps-jupyterlab + - fps-contents + - fps-kernels + - fps-frontend diff --git a/kernel_widget.py b/kernel_widget.py index 57920a6..40669d2 100644 --- a/kernel_widget.py +++ b/kernel_widget.py @@ -1,41 +1,36 @@ """ README: - - this is a proxy application for an actual qt application which wants to intergrate - jupyter +- this is a proxy application for an actual qt application which wants to intergrate + jupyter - - code to execute: +- code to execute: - from kernel_widget import get_kernel_widget - from PyQt5.QtWidgets import QPushButton - button = QPushButton() - button.setText("black magic") +from kernel_widget import get_kernel_widget +from PyQt5.QtWidgets import QPushButton +button = QPushButton() +button.setText("black magic") - def say_hello(): - print("hello from here") +def say_hello(): + print("hello from here") - button.clicked.connect(say_hello) +button.clicked.connect(say_hello) - get_kernel_widget().layout.addWidget(button) +get_kernel_widget().layout.addWidget(button) """ import json -import os from pathlib import Path import sys import tempfile -import time import subprocess import xqtpython import socket from contextlib import closing from types import ModuleType -import PyQt5 from PyQt5.QtCore import QUrl, QTimer from PyQt5.QtWidgets import ( QApplication, - QHBoxLayout, QVBoxLayout, - QPushButton, QWidget, ) from PyQt5.QtWebEngineWidgets import QWebEngineView @@ -53,14 +48,13 @@ class KernelWidget(QWidget): A webview widget showing a jupyterlab instance """ - def __init__(self, kernel_name, jupyverse_dir, *args, **kwargs): + def __init__(self, kernel_name, *args, **kwargs): super(KernelWidget, self).__init__(*args, **kwargs) self.layout = QVBoxLayout() self.setLayout(self.layout) self.kernel_name = kernel_name - self.jupyverse_dir = jupyverse_dir # browser self.browser = QWebEngineView() @@ -112,25 +106,24 @@ def _start_kernel(self): def _start_jupyverse(self): # self.start_server_button.setDisabled(True) self.server_port = find_free_port() + token = "my_token" - # atm we still run a dev version of jupyverse args = [ - "hatch", - "run", - "dev.jupyterlab-noauth:jupyverse", - f"--kernels.connection_path={str(self.kernel_file_dir.name)}", - "--port", - f"{self.server_port}", + "jupyverse", + "--set", "kernels.allow_external_kernels=true", + "--set" ,f"kernels.external_connection_dir={str(self.kernel_file_dir.name)}", + "--set" ,f"auth.token={token}", + "--port", f"{self.server_port}", ] self.server_process = subprocess.Popen( - args, cwd=self.jupyverse_dir, shell=False + args, shell=False ) # we need to wait a tiny bit st the page is ready def setUrl(): - self.browser.setUrl(QUrl(f"http://127.0.0.1:{self.server_port}")) + self.browser.setUrl(QUrl(f"http://127.0.0.1:{self.server_port}/?token={token}")) - QTimer.singleShot(1000, setUrl) + QTimer.singleShot(2000, setUrl) def closeEvent(self, event): # do stuff @@ -141,20 +134,8 @@ def closeEvent(self, event): if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser( - prog="kernel-demo", description="show a kernel in qt application" - ) - - parser.add_argument("jupyverse_dir") - - args = parser.parse_args() - app = QApplication(sys.argv) - kernel_widget = KernelWidget( - kernel_name="qt-python", jupyverse_dir=args.jupyverse_dir - ) + kernel_widget = KernelWidget(kernel_name="qt-python") kernel_widget.show() diff --git a/my_notebook.ipynb b/my_notebook.ipynb new file mode 100644 index 0000000..085518c --- /dev/null +++ b/my_notebook.ipynb @@ -0,0 +1,77 @@ +{ + "metadata": { + "kernelspec": { + "name": "qt-python" + }, + "language_info": { + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "version": "3.12.0rc3" + } + }, + "nbformat_minor": 5, + "nbformat": 4, + "cells": [ + { + "id": "82774918-e61b-4383-b44d-ad2739ff5e34", + "cell_type": "code", + "source": "from kernel_widget import get_kernel_widget\nfrom PyQt5.QtWidgets import QPushButton", + "metadata": { + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "f249770b-d42c-4432-953c-36ae94ee850b", + "cell_type": "code", + "source": "button = QPushButton()\nbutton.setText(\"black magic\")", + "metadata": { + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "9aa5486b-ff9b-46a4-b661-b2634bcbdec8", + "cell_type": "code", + "source": "def say_hello():\n print(\"hello from here\")", + "metadata": { + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "ab7d7956-10d3-4032-9e8f-9b67ac5984b1", + "cell_type": "code", + "source": "button.clicked.connect(say_hello)", + "metadata": { + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "02ee9a0a-27d1-4706-8e9c-32fed24bf24f", + "cell_type": "code", + "source": "get_kernel_widget().layout.addWidget(button)", + "metadata": { + "trusted": true + }, + "outputs": [], + "execution_count": null + }, + { + "id": "2d724999-921f-438c-afa8-aeef905e4ef6", + "cell_type": "code", + "source": "", + "metadata": { + "trusted": true + }, + "outputs": [], + "execution_count": null + } + ] +} \ No newline at end of file diff --git a/pyside_kernel_widget.py b/pyside_kernel_widget.py index 30390f1..dd991f8 100644 --- a/pyside_kernel_widget.py +++ b/pyside_kernel_widget.py @@ -1,41 +1,36 @@ """ README: - - this is a proxy application for an actual qt application which wants to intergrate - jupyter +- this is a proxy application for an actual qt application which wants to intergrate + jupyter - - code to execute: +- code to execute: - from kernel_widget import get_kernel_widget - from PySide2.QtWidgets import QPushButton - button = QPushButton() - button.setText("black magic") +from kernel_widget import get_kernel_widget +from PySide2.QtWidgets import QPushButton +button = QPushButton() +button.setText("black magic") - def say_hello(): - print("hello from here") +def say_hello(): + print("hello from here") - button.clicked.connect(say_hello) +button.clicked.connect(say_hello) - get_kernel_widget().layout.addWidget(button) +get_kernel_widget().layout.addWidget(button) """ import json -import os from pathlib import Path import sys import tempfile -import time import subprocess import xqtpython import socket from contextlib import closing from types import ModuleType -import PySide2 from PySide2.QtCore import QUrl, QTimer from PySide2.QtWidgets import ( QApplication, - QHBoxLayout, QVBoxLayout, - QPushButton, QWidget, ) from PySide2.QtWebEngineWidgets import QWebEngineView @@ -53,14 +48,13 @@ class KernelWidget(QWidget): A webview widget showing a jupyterlab instance """ - def __init__(self, kernel_name, jupyverse_dir, *args, **kwargs): + def __init__(self, kernel_name, *args, **kwargs): super(KernelWidget, self).__init__(*args, **kwargs) self.layout = QVBoxLayout() self.setLayout(self.layout) self.kernel_name = kernel_name - self.jupyverse_dir = jupyverse_dir # browser self.browser = QWebEngineView() @@ -96,7 +90,7 @@ def get_kernel_widget(): def _start_kernel(self): print("start kernel") self.kernel = xqtpython.xkernel( - redirect_output_enabled=False, + redirect_output_enabled=True, redirect_display_enabled=True, ) @@ -113,25 +107,24 @@ def _start_kernel(self): def _start_jupyverse(self): # self.start_server_button.setDisabled(True) self.server_port = find_free_port() + token = "my_token" - # atm we still run a dev version of jupyverse args = [ - "hatch", - "run", - "dev.jupyterlab-noauth:jupyverse", - f"--kernels.connection_path={str(self.kernel_file_dir.name)}", - "--port", - f"{self.server_port}", + "jupyverse", + "--set", "kernels.allow_external_kernels=true", + "--set" ,f"kernels.external_connection_dir={str(self.kernel_file_dir.name)}", + "--set" ,f"auth.token={token}", + "--port", f"{self.server_port}", ] self.server_process = subprocess.Popen( - args, cwd=self.jupyverse_dir, shell=False + args, shell=False ) # we need to wait a tiny bit st the page is ready def setUrl(): - self.browser.setUrl(QUrl(f"http://127.0.0.1:{self.server_port}")) + self.browser.setUrl(QUrl(f"http://127.0.0.1:{self.server_port}/?token={token}")) - QTimer.singleShot(1000, setUrl) + QTimer.singleShot(2000, setUrl) def closeEvent(self, event): # do stuff @@ -142,20 +135,8 @@ def closeEvent(self, event): if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser( - prog="kernel-demo", description="show a kernel in qt application" - ) - - parser.add_argument("jupyverse_dir") - - args = parser.parse_args() - app = QApplication(sys.argv) - kernel_widget = KernelWidget( - kernel_name="qt-python", jupyverse_dir=args.jupyverse_dir - ) + kernel_widget = KernelWidget(kernel_name="qt-python") kernel_widget.show()