Source code for sardana.taurus.qt.qtgui.extra_sardana.qtspock
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##############################################################################
##
# This file is part of Sardana
##
# http://www.sardana-controls.org/
##
# Copyright 2020 DESY
##
# Sardana is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
##
# Sardana is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
##
# You should have received a copy of the GNU Lesser General Public License
# along with Sardana. If not, see <http://www.gnu.org/licenses/>.
##
##############################################################################
"""A RichJupyterWidget that loads a spock profile.
.. note::
The `qtspock` module has been included in Sardana
on a provisional basis. Backwards incompatible changes
(up to and including its removal) may occur if
deemed necessary by the core developers.
"""
import sys
import pickle
import ast
from typing import Any, Optional
from packaging.version import Version
import traitlets
from IPython.core.profiledir import ProfileDirError, ProfileDir
try:
import qtconsole
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtconsole.manager import QtKernelManager
except ImportError:
qtconsole = None
from taurus.external.qt import Qt
from taurus import info, error
from taurus.qt.qtgui.base import TaurusBaseWidget
from taurus.qt.qtgui.container import TaurusMainWindow
from taurus.qt.qtgui.resource import getThemeIcon
from sardana import release
from sardana.spock.ipython_01_00.genutils import get_profile_metadata, \
get_ipython_dir, from_name_to_tango, get_macroserver_for_door
from sardana.taurus.qt.qtgui.extra_macroexecutor import \
TaurusMacroConfigurationDialog
NO_QTCONSOLE_MESSAGE = (
"QtSpock cannot be displayed (qtconsole is not installed)"
)
def assertQtconsoleAvailability(exit_on_error=True):
if qtconsole is None:
Qt.QMessageBox.critical(None, 'QtSpock error', NO_QTCONSOLE_MESSAGE)
if exit_on_error:
exit(1)
else:
import jupyter_client
NOCOMPAT_JUPYTERCLIENT_MESSAGE = \
"QtSpock cannot be displayed (jupyter_client > 6.1.12 is not supported)"
if Version(jupyter_client.__version__) > Version("6.1.12"):
Qt.QMessageBox.critical(
None, 'QtSpock error', NOCOMPAT_JUPYTERCLIENT_MESSAGE
)
if exit_on_error:
exit(1)
def get_spock_profile_dir(profile):
"""Return the path to the profile with the given name."""
try:
profile_dir = ProfileDir.find_profile_dir_by_name(
get_ipython_dir(), profile)
except ProfileDirError:
return None
return profile_dir.location
def check_spock_profile(profile):
"""Check if the profile exists and has the correct value"""
profile_dir = get_spock_profile_dir(profile)
if profile_dir:
profile_version_str, door_name = get_profile_metadata(profile_dir)
if profile_version_str == release.version:
return True
return False
class SpockKernelManager(QtKernelManager):
"""
A kernel manager that checks the spock profile before starting a kernel.
.. note::
The `SpockKernelManager` class has been included in Sardana
on a provisional basis. Backwards incompatible changes
(up to and including its removal) may occur if
deemed necessary by the core developers.
If the check fails, i.e., the profile does not exist or has a different
version, an ipython kernel without spock functionality is started instead
and the attribute `valid_spock_profile` is set to `False`.
"""
kernel_about_to_launch = Qt.pyqtSignal()
def _launch_kernel(self, kernel_cmd, **kw):
try:
profile = kernel_cmd[kernel_cmd.index("--profile") + 1]
except ValueError:
self.is_valid_spock_profile = False
else:
if check_spock_profile(profile):
self.is_valid_spock_profile = True
else:
index = kernel_cmd.index("--profile")
del kernel_cmd[index]
del kernel_cmd[index]
for arg in kernel_cmd[:]:
if arg.startswith("--Spock"):
kernel_cmd.remove(arg)
self.is_valid_spock_profile = False
error("Checking spock profile failed.")
info('Starting kernel...')
self.kernel_about_to_launch.emit()
return super()._launch_kernel(kernel_cmd, **kw)
[docs]
class QtSpockWidget(RichJupyterWidget, TaurusBaseWidget):
"""A RichJupyterWidget that starts a kernel with a spock profile.
.. note::
The `QtSpockWidget` class has been included in Sardana
on a provisional basis. Backwards incompatible changes
(up to and including its removal) may occur if
deemed necessary by the core developers.
It is important to call `shutdown_kernel` to gracefully clean up the
started subprocesses.
Useful methods of the base class include execute, interrupt_kernel,
restart_kernel, and clear.
:param profile:
The name of the spock profile to use. The default is 'spockdoor'.
:param kernel:
The name of the kernel to use. The default is 'python3'.
:param use_model_from_profile:
If true, the door name is taken from the spock profile, otherwise it
has to be set via setModel.
:param kwargs:
All remaining keywords are passed to the RichJupyterWidget base class
Examples::
from taurus.external.qt import Qt
from sardana.taurus.qt.qtgui.extra_sardana.qtspock import QtSpockWidget
app = Qt.QApplication(["qtspock"])
widget = QtSpockWidget(use_model_from_profile=True)
widget.show()
widget.start_kernel()
app.aboutToQuit.connect(widget.shutdown_kernel)
app.exec_()
"""
def __init__(
self,
parent: Any = None,
profile: str = 'spockdoor',
use_model_from_profile: bool = False,
extensions: Any = None,
kernel: str = 'python3',
**kw: Any) -> None:
RichJupyterWidget.__init__(self, parent=parent, **kw)
TaurusBaseWidget.__init__(self)
self.setObjectName(self.__class__.__name__)
self.setModelInConfig(True)
self._profile = profile
self.use_model_from_profile = use_model_from_profile
if extensions is None:
extensions = []
extensions.insert(
0, "sardana.taurus.qt.qtgui.extra_sardana.qtspock_ext")
self._extensions = extensions
self._kernel_name = kernel
self._macro_server_name = None
self._macro_server_alias = None
self._door_name = None
self._door_alias = None
self._config_passed_as_extra_arguments = False
self.append_stream("Waiting for kernel to start")
self.kernel_manager = SpockKernelManager(kernel_name=kernel)
self.kernel_manager.kernel_about_to_launch.connect(
self._handle_kernel_lauched)
[docs]
def start_kernel(self):
"""Start the kernel
A normal IPython kernel is started if no model is set via `setModel` or
`use_model_from_profile`.
"""
if not self.kernel_manager.has_kernel:
self.kernel_manager.start_kernel(
extra_arguments=self._extra_arguments())
kernel_client = self.kernel_manager.client()
kernel_client.start_channels()
self.kernel_client = kernel_client
def _extra_arguments(self):
extra_arguments = ["--profile", self._profile]
for ext in self._extensions:
extra_arguments.extend(["--ext", ext])
if not self.use_model_from_profile:
if self._macro_server_name and self._door_name:
self._config_passed_as_extra_arguments = True
extra_arguments.append("--Spock.macro_server={}".format(
self._macro_server_name))
extra_arguments.append("--Spock.macro_server_alias={}".format(
self._macro_server_alias))
extra_arguments.append("--Spock.door_name={}".format(
self._door_name))
extra_arguments.append("--Spock.door_alias={}".format(
self._door_alias))
else:
# Loading the spock profile would use the macro server and door
# configured there. Instead, use no extra arguments for an
# ipython kernel without Spock functionality
extra_arguments = []
return extra_arguments
[docs]
def setModel(self, door):
"""Set a door as the model
An empty string or None will start a normal IPython kernel without
spock functionality.
"""
old_door_name = self._door_name
old_macroserver_name = self._macro_server_name
self._set_door_name(door)
self._set_macro_server_name(door)
if (self._door_name == old_door_name
and self._macro_server_name == old_macroserver_name):
return
if self.kernel_manager.has_kernel:
# RichJupyterWidget.restart_kernel does not support extra arguments
self.kernel_manager.restart_kernel(
extra_arguments=self._extra_arguments())
self._kernel_restarted_message(died=False)
else:
self.start_kernel()
def _set_door_name(self, door):
if door:
full_door_tg_name, door_tg_name, door_tg_alias = (
from_name_to_tango(door))
door_alias = door_tg_alias or door_tg_name
self._door_name = full_door_tg_name
self._door_alias = door_alias
else:
self._door_name = None
self._door_alias = None
def _set_macro_server_name(self, door):
if door:
macro_server = get_macroserver_for_door(door)
full_ms_tg_name, ms_tg_name, ms_tg_alias = (
from_name_to_tango(macro_server))
ms_alias = ms_tg_alias or ms_tg_name
self._macro_server_name = full_ms_tg_name
self._macro_server_alias = ms_alias
else:
self._macro_server_name = None
self._macro_server_alias = None
def _set_prompts(self):
# If traitlets >= 5.0.0 then DeferredConfigString is used for values
# that are not listed in the configurable classes. Get its value.
if (traitlets.version_info >= (5, 0, 0)
and self._config_passed_as_extra_arguments):
self.kernel_client.execute(
"from sardana.spock.config import Spock", silent=True)
var = "get_ipython().config.Spock.door_alias.get_value(Spock.door_alias)" # noqa
else:
var = "get_ipython().config.Spock.door_alias"
self._silent_exec_callback(
var, self._set_prompts_callback)
def _set_prompts_callback(self, msg):
in_prefix = 'In'
if msg['status'] == 'ok':
output_bytes = msg['data']['text/plain']
try:
in_prefix = ast.literal_eval(output_bytes)
except SyntaxError:
pass
if not self.kernel_manager.is_valid_spock_profile:
self._print_ipython_warning()
self.in_prompt = (
in_prefix + ' [<span class="in-prompt-number">%i</span>]:')
self.out_prompt = (
'Result [<span class="out-prompt-number">%i</span>]:')
def _print_ipython_warning(self):
if self.use_model_from_profile or self._extra_arguments():
self.append_stream(
"\nSpock profile error: please run spock in the terminal"
" and restart the kernel.\n")
else:
self.append_stream(
"\nNo door selected. Please select a valid door.\n")
self.append_stream(
"\nThis is a normal ipython kernel. "
"Spock functionality is not available.\n")
# Adapted from
# https://github.com/moble/remote_exec/blob/master/remote_exec.py#L61
[docs]
def get_value(self, var: str, timeout: Optional[int] = None) -> Any:
"""Retrieve a value from the user namespace through a blocking call.
The value must be able to be pickled on the kernel side and unpickled
on the frontend side.
The command will import the pickle module in the user namespace. This
may overwrite a user defined variable with the same name.
:param var:
The name of the variable to be retrieved
:param timeout:
Number of seconds to wait for reply. If no reply is recieved, a
`Queue.Empty` exception is thrown. The default is to wait
indefinitely
:return:
The value of the variable from the user namespace
"""
pickle_dumps = 'pickle.dumps({})'.format(var)
msg_id = self.blocking_client.execute(
"import pickle", silent=True,
user_expressions={'output': pickle_dumps})
reply = self.blocking_client.get_shell_msg(msg_id, timeout=timeout)
if reply['content']['status'] != "ok":
raise RuntimeError("{}: {}".format(
reply['content']['ename'], reply['content']['evalue']))
output = reply['content']['user_expressions']['output']
if output['status'] != "ok":
raise RuntimeError("{}: {}".format(
output['ename'], output['evalue']))
output_bytes = output['data']['text/plain']
output_bytes = ast.literal_eval(output_bytes)
return pickle.loads(output_bytes)
[docs]
def shutdown_kernel(self):
"""Cleanly shut down the kernel and client subprocesses"""
info('Shutting down kernel...')
if self.kernel_client:
self.kernel_client.stop_channels()
if self.kernel_manager and self.kernel_manager.has_kernel:
self.kernel_manager.shutdown_kernel()
def _handle_kernel_lauched(self):
if self.kernel_client:
self.kernel_client.kernel_info()
def _handle_kernel_info_reply(self, rep):
self._set_prompts()
is_starting = self._starting
super()._handle_kernel_info_reply(rep)
if not is_starting:
# The base method did not print the banner and reset the prompt.
# As the profile might have changed, do it here.
self._append_plain_text("\n\n")
self._append_plain_text(self.kernel_banner)
self.reset()
[docs]
class QtSpock(TaurusMainWindow):
"""A standalone QtSpock window
.. note::
The `QtSpock` class has been included in Sardana
on a provisional basis. Backwards incompatible changes
(up to and including its removal) may occur if
deemed necessary by the core developers.
"""
def __init__(self, parent=None, designMode=False):
super().__init__(parent, designMode)
self.spockWidget = QtSpockWidget(parent=self)
self.registerConfigDelegate(self.spockWidget)
self.spockWidget.setModelInConfig(True)
self.setCentralWidget(self.spockWidget)
self.configureAction = self.createConfigureAction()
self.taurusMenu.addAction(self.configureAction)
self.statusBar().showMessage("QtSpock ready")
self.loadSettings()
[docs]
def createConfigureAction(self):
configureAction = Qt.QAction(getThemeIcon(
"preferences-system-session"), "Change configuration", self)
configureAction.triggered.connect(self.changeConfiguration)
configureAction.setToolTip("Configuring MacroServer and Door")
configureAction.setShortcut("F10")
return configureAction
[docs]
def changeConfiguration(self):
"""This method is used to change macroserver as a model of application.
It shows dialog with list of all macroservers on tango host, if the
user Cancel dialog it doesn't do anything."""
dialog = TaurusMacroConfigurationDialog(
self, self.spockWidget._macro_server_name,
self.modelName)
if dialog.exec_():
self.spockWidget.setModel(str(dialog.doorComboBox.currentText()))
else:
return
def main():
from taurus.qt.qtgui.application import TaurusApplication
app = TaurusApplication()
app.setOrganizationName("Taurus")
app.setApplicationName("QtSpock")
assertQtconsoleAvailability()
window = QtSpock()
window.show()
app.aboutToQuit.connect(window.spockWidget.shutdown_kernel)
sys.exit(app.exec_())
if __name__ == "__main__":
main()