Commit ce7d149c authored by Nigel Palmer's avatar Nigel Palmer
Browse files

Added content for the Flare Python 3.0 release.

parent 347dddf3
# Changelog
## [3.0] - 2020-01-30
### Changed
- Updates to support the Flare 3.0 Python API
### Removed
- The Python Notebook extension has been moved to the flare-python-extensions/featured repository
## [2.0] - 2018-07-25
### Added
- Example on how to create a GUI using QtDesigner (qtdesignerexample)
......
......@@ -8,7 +8,7 @@ The developer example extensions are provided to you on an "AS-IS" basis, howeve
## Installing
See the [installation guide](https://gitlab.com/cresset/flare-python-extensions/README.md).
See the [installation guide](https://gitlab.com/cresset/flare-python-extensions/blob/master/README.md).
## Extensions Descriptions
......@@ -45,50 +45,6 @@ Demonstrates how to use a rest server to add properties to Ligand table when lig
### Ribbon (ribbon)
Demonstrates how to create a ribbon tab and group containing various buttons and widgets.
### Python Notebook (pythonnotebook)
Add the Jupyter-based Python Notebook to Flare.
To install, copy the whole `pythonnotebook` directory to your `extensions` directory,
whose location is platform-specific:
Windows: `%LOCALAPPDATA%\Cresset BMD\Flare\python\extensions`
Linux: `$HOME/.com.cresset-bmd/Flare/python/extensions`
macOS: `$HOME/Library/Application Support/Cresset BMD/Flare/python/extensions`
Make sure that `jupyter` and its dependencies are installed in your `pyflare`
installation. The location of the `pyflare` binary depends on where Flare was
installed on the different platforms:
Windows:
* `%PROGRAMFILES%\Cresset-BMD\Flare\pyflare.exe` (system-wide install)
* `%LOCALAPPDATA%\Cresset-BMD\Flare\pyflare.exe` (user-level install)
Linux:
* `/opt/cresset/Flare/bin/pyflare` (system-wide RPM install)
* `/your/path/cresset/Flare/bin/pyflare` (user-level TGZ install, `/your/path` is user-determined)
macOS:
* `/Applications/Flare.app/Contents/MacOS/pyflare` (system-wide install)
* `$HOME/Applications/Flare.app/Contents/MacOS/pyflare` (user-level install)
Once you have identified where the `pyflare` binary lives on your system, replace
`/path/to` in the command reported below with the relevant path on your system:
```
pyflare -m pip install --user jupyter pillow
```
Please note that currently the Python Notebook extension is not compatible
with the Python QtConsole extension.
Therefore, if you wish to use the Python Notebook extension make sure that
your `extensions` directory does not contain the `pythonqtconsole` directory.
Conversely, if you prefer to use the Python QtConsole, make sure that your
`extensions` directory does not contain the `pythonnotebook` directory.
To enable the Python Notebook, click on the Python Notebook button
(the one with the Jupyter icon) in the "Code" group of the "Python" ribbon.
## Scripts Descriptions
### Run REST Service (runrestservice.py)
......
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Copyright (C) 2020 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Demonstrates how to add items to context menus."""
from PySide2 import QtCore, QtWidgets
from cresset import flare
from cressetdeveloperutils import report_error
@flare.extension
......@@ -31,50 +30,40 @@ class ContextMenusExtension:
action = menu.addAction("Atom Python Menu")
action.triggered.connect(
report_error(
lambda: self._show_dialog(
f"{len(atoms)} current atoms.\n{len(picked_atoms)} picked atoms."
)
lambda: self._show_dialog(
f"{len(atoms)} current atoms.\n{len(picked_atoms)} picked atoms."
)
)
def _ligand_menu(self, menu, ligand):
action = menu.addAction("Ligand Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Opened on '{ligand.title}'."))
)
action.triggered.connect(lambda: self._show_dialog(f"Opened on '{ligand.title}'."))
def _pose_menu(self, menu, pose):
action = menu.addAction("Pose Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Pose SMILES '{pose.smiles()}'."))
)
action.triggered.connect(lambda: self._show_dialog(f"Pose SMILES '{pose.smiles()}'."))
def _role_menu(self, menu, role):
action = menu.addAction("Role Python Menu")
action.triggered.connect(report_error(lambda: self._show_dialog(f"Role '{role.name}'.")))
action.triggered.connect(lambda: self._show_dialog(f"Role '{role.name}'."))
def _ligand_table_header_menu(self, menu, header):
action = menu.addAction("Header Python Menu")
action.triggered.connect(report_error(lambda: self._show_dialog(f"Header '{header}'.")))
action.triggered.connect(lambda: self._show_dialog(f"Header '{header}'."))
def _protein_residue_menu(self, menu, residue):
action = menu.addAction("Residue Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Residue '{str(residue)}'."))
)
action.triggered.connect(lambda: self._show_dialog(f"Residue '{str(residue)}'."))
def _protein_sequence_menu(self, menu, sequence):
action = menu.addAction("Sequence Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Sequence '{sequence.chain} {sequence.name}'."))
lambda: self._show_dialog(f"Sequence '{sequence.chain} {sequence.name}'.")
)
def _protein_menu(self, menu, protein):
action = menu.addAction("Protein Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Protein '{protein.title}'."))
)
action.triggered.connect(lambda: self._show_dialog(f"Protein '{protein.title}'."))
@staticmethod
def _show_dialog(text):
......
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Provides utility functions used by other scripts."""
import traceback
from cresset import flare
from PySide2 import QtCore, QtWidgets
def report_error(func):
"""Return a function which calls `func` and shows an error dialog if there an error."""
# noqa: D202
def show_error_dialog_on_exception(*args, **kw):
"""Show an error dialog if `func` raises an exception."""
try:
func(*args, **kw)
except Exception as e:
parent = flare.main_window().widget()
box = QtWidgets.QMessageBox(parent)
box.setAttribute(QtCore.Qt.WA_DeleteOnClose)
box.setIcon(QtWidgets.QMessageBox.Critical)
box.setDetailedText(traceback.format_exc())
box.setText(str(e))
box.exec_()
raise # Raise the error so its reported in the python log
return show_error_dialog_on_exception
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Copyright (C) 2020 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Demonstrates how to download a protein from a remote server.
......@@ -11,7 +11,6 @@ Ribbon Controls:
import urllib.request
from cresset import flare
from cressetdeveloperutils import report_error
@flare.extension
......@@ -20,9 +19,12 @@ class Download1oitExtension:
def load(self):
"""Load the extension."""
group = flare.main_window().ribbon["Developer"]["Download"]
control = group.add_button("1oit", report_error(self._download_1oit))
tab = flare.main_window().ribbon["Developer"]
tab.tip_key = "D"
group = tab["Download"]
control = group.add_button("1oit", self._download_1oit)
control.tooltip = "Download the protein '1oit' from the RCSB."
control.tip_key = "D"
print(f"Loaded {self.__class__.__module__}.{self.__class__.__name__}")
......
# -*- coding: utf-8 -*-
# Copyright (C) 2019 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Add the Jupyter-based Python Notebook to Flare.
To install, copy the whole "pythonnotebook" directory to your "extensions" directory,
which is in the respective locations on Windows, Linux and macOS:
Windows: %LOCALAPPDATA%\Cresset BMD\Flare\python\extensions
Linux: $HOME/.com.cresset-bmd/Flare/python/extensions
macOS: $HOME/Library/Application Support/Cresset BMD/Flare/python/extensions
Make sure that jupyter and its dependencies are installed in your pyflare
installation. The location of the pyflare binary depends on where Flare was
installed on the different platforms:
Windows:
* %PROGRAMFILES%\Cresset-BMD\Flare\pyflare.exe (system-wide install)
* %LOCALAPPDATA%\Cresset-BMD\Flare\pyflare.exe (user-level install)
Linux:
* /opt/cresset/Flare/bin/pyflare (system-wide RPM install)
* /your/path/cresset/Flare/bin/pyflare (user-level TGZ install, /your/path is user-determined)
macOS:
* /Applications/Flare.app/Contents/MacOS/pyflare (system-wide install)
* $HOME/Applications/Flare.app/Contents/MacOS/pyflare (user-level install)
Once you have identified where the pyflare binary lives on your system, replace
/path/to in the command reported below with the relevant path on your system:
pyflare -m pip install jupyter pillow
Please note that currently the Python Notebook extension is not compatible
with the Python QtConsole extension.
Therefore, if you wish to use the Python Notebook extension make sure that
your "extensions" directory does not contain the "pythonqtconsole" directory.
Conversely, if you prefer to use the Python QtConsole, make sure that your
"extensions" directory does not contain the "pythonnotebook" directory.
To enable the Python Notebook, click on the Python Notebook button
(the one with the Jupyter icon) in the "Code" group of the "Python" ribbon.
"""
import os
from .configurelogging import get_logger
from .common import flare_extension_dir
log = get_logger("FlareJupyter")
# The NotebookPreload.py script is executed before anything else
# This is useful, for example, for things that need to be monkey-patched
# before anything else happens
preload_script_path = os.path.join(flare_extension_dir(), "notebookpreload.py")
preload_script = None
try:
with open(preload_script_path, "r") as hnd:
preload_script = hnd.read()
except Exception:
log.warning("Failed to open preload script {0:s} for reading".format(preload_script_path))
if preload_script:
exec(preload_script)
from cresset import flare
from qtconsole.qt import QtCore, QtGui
from .flarenotebook import PythonNotebookWidget
@flare.extension
class PythonNotebookExtension:
"""Flare extension class."""
PYTHON_NOTEBOOK_APPNAME = "Python Notebook"
def load(self):
"""Load in this Flare extension."""
self.init()
print("Loaded {0:s}.{1:s}".format(self.__class__.__module__, self.__class__.__name__))
def py_notebook_button_toggled(self):
"""Call when the Python Notebook ribbon button is toggled."""
if self.py_notebook_button_q_action.isChecked():
self.py_notebook.py_notebook_show()
else:
self.py_notebook.py_notebook_hide()
def init(self):
"""Initialize PythonNotebookExtension."""
QtGui.qApp = QtGui.QApplication
# FIXME: the following line might not be needed any more once
# https://github.com/jupyter/qtconsole/issues/279
# is closed and incorporated in the main qt-console trunk
QtGui.QApplication.instance().font = QtGui.QApplication.font
self.py_notebook = PythonNotebookWidget(None, QtCore.Qt.Window)
self.py_notebook.setWindowTitle(self.PYTHON_NOTEBOOK_APPNAME)
self.py_notebook_button_ui_control = (
flare.main_window()
.ribbon["Python"]["Code"]
.add_button(
self.PYTHON_NOTEBOOK_APPNAME.replace(" ", "\n"), self.py_notebook_button_toggled
)
)
self.py_notebook_button_ui_control.tooltip = "Open the Python Notebook."
self.py_notebook_button_q_action = self.py_notebook_button_ui_control.action()
if not self.py_notebook_button_q_action:
raise RuntimeError("Could not create PythonNotebook button")
icon_file = os.path.join(flare_extension_dir(), "notebook.png")
if os.path.isfile(icon_file):
self.py_notebook_button_ui_control.load_icon(icon_file)
self.py_notebook_button_q_action.setCheckable(True)
self.py_notebook.set_py_notebook_button_action(self.py_notebook_button_q_action)
self.py_notebook_button_q_action.setChecked(False)
self.py_notebook.hide()
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Common utility classes."""
import os
import traceback
from qtconsole.qt import QtCore, QtGui
from .configurelogging import get_logger
log = get_logger("common")
def flare_extension_dir():
"""Return the directory we are in, i.e. the Flare Python extensions dir."""
return os.path.dirname(os.path.realpath(__file__))
class DisableWidget:
"""Disable widget on enter; re-enable it on exit."""
def __init__(self, widget):
self._widget = widget
def __enter__(self):
if self._widget:
self._widget.setEnabled(False)
return self
def __exit__(self, exc_type, exc_value, trace_back):
if self._widget:
self._widget.setEnabled(True)
class FileStatMonitor(QtGui.QProgressDialog):
"""Class which monitors change in a file and displays a progressbar."""
MAX_ATTEMPTS = 3
WAIT_FOR_MS = 100
FIRST_TIMEOUT = 1000
SECOND_TIMEOUT = 500
class CannotAccess(Exception):
"""Exception thrown if the file cannot be accessed."""
pass
class NoChange(Exception):
"""Exception thrown if the file has not changed."""
pass
class Empty(Exception):
"""Exception thrown if the file has zero content."""
pass
def _start_monitoring(self):
"""Get the initial stat of the file, then call _init_func if not None."""
self._prev_stat = None
try:
self._prev_stat = os.stat(self._file)
except Exception:
pass
if self._init_func:
self._init_func()
def wait_until_stable(self, timeout=FIRST_TIMEOUT, max_attempts=MAX_ATTEMPTS):
"""Wait until the file stat has stabilized or timeout occurs.
MAX_ATTEMPTS to detect file change are made; if no change takes place,
a NoChange exception is thrown. The other exceptions are thrown upon
first failure.
"""
attempts = 0
max_attempts = self.MAX_ATTEMPTS
success = False
p = 0
maxp = self.FIRST_TIMEOUT + self.SECOND_TIMEOUT
has_changed = False
still_changing = False
now_existing = False
while (not success) and (attempts < max_attempts):
attempts += 1
self._start_monitoring()
t = timeout
with DisableWidget(self._parent):
while t:
v = min(100, int(100.0 * p / maxp))
log.debug("wait_until_stable v = {0:d}".format(v))
if self._parent.isVisible():
self.setValue(v)
QtGui.QApplication.instance().processEvents()
if os.path.isfile(self._file):
if not now_existing:
log.debug("now existing")
t = timeout
now_existing = True
try:
current_stat = os.stat(self._file)
except Exception:
log.warning(
"wait_until_stable exception:\n{0:s}".format(
"".join(traceback.format_exc())
)
)
raise self.CannotAccess
still_changing = (not self._prev_stat) or (
current_stat.st_mtime != self._prev_stat.st_mtime
)
if still_changing:
log.debug("wait_until_stable still_changing")
if not has_changed:
log.debug("wait_until_stable has_changed = True")
has_changed = True
self._prev_stat = current_stat
t = self.SECOND_TIMEOUT
t -= self.WAIT_FOR_MS
p += self.WAIT_FOR_MS / attempts
log.debug("wait_until_stable t = {0:d}".format(t))
QtCore.QThread.msleep(self.WAIT_FOR_MS)
if not self._prev_stat.st_size:
raise self.Empty
success = has_changed and (not still_changing)
if self._parent.isVisible():
self.setValue(100)
self.deleteLater()
if not has_changed:
raise self.NoChange
def __init__(self, file, parent=None, text="Preparing...", init_func=None):
self._file = file
self._parent = parent
self._init_func = init_func
super().__init__(self._parent, QtCore.Qt.Widget)
self.setCancelButton(None)
self.setMinimumDuration(0)
self.setLabelText(text)
self._should_show = self._parent.isVisible()
if self._should_show:
self.forceShow()
else:
self.hide()
self.lower()
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Configure logging level and log file location."""
import os
import logging
import tempfile
def get_logger(logger="default"):
"""Return logger appropriate for the logging level."""
is_debug_run = os.environ.get("CRESSET_IPYNB_DEBUG", None)
logging_level = logging.DEBUG if is_debug_run else logging.WARNING
logging.basicConfig(
filename=os.path.join(tempfile.gettempdir(), "flare_ipynb.log"), level=logging_level
)
return logging.getLogger(logger)
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Classes to get/set the last visited directory."""
from qtconsole.qt import QtCore
class DirectoryService:
"""Static class to get/set last visited directory."""
@staticmethod
def getLastVisitedDir():
"""Get the last visited directory.
Returns None if none available, or a QDir if available.
"""
settings = QtCore.QSettings()
cwd = settings.value("lastVisitedDir", None)
if cwd:
cwd = QtCore.QDir(cwd)
if (not cwd) or (not cwd.exists()):
documents = QtCore.QStandardPaths.standardLocations(
QtCore.QStandardPaths.DocumentsLocation
)
if documents:
cwd = QtCore.QDir(documents[0])
return cwd
@staticmethod
def _setLastVisitedDirFromQDir(qdir):
"""Set the last visited directory from an argument of type QDir."""
if qdir.exists():
settings = QtCore.QSettings()
settings.setValue("lastVisitedDir", qdir.absolutePath())
@staticmethod
def setLastVisitedDir(arg):
"""Set the last visited directory from an argument of type QDir or QFileInfo."""
if isinstance(arg, QtCore.QDir):
DirectoryService._setLastVisitedDirFromQDir(arg)
elif isinstance(arg, QtCore.QFileInfo):
if arg.isDir():
DirectoryService._setLastVisitedDirFromQDir(arg.absoluteDir())
else:
DirectoryService._setLastVisitedDirFromQDir(arg.dir())
else:
raise TypeError("Expected QDir or QFileInfo, got {0:s}".format(type(arg)))
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Classes to manage integration between IPython kernel and Qt event loop."""
import ctypes
from cresset import flare
from qtconsole.qt import QtCore, QtGui
from .configurelogging import get_logger
log = get_logger("eventloop")
class EventLoopHelper(object):
"""Singleton class to process Qt events while Python code is running in the IPython kernel."""
MAX_RESOURCES = 1
_instance = None
def __new__(cls):
"""Call upon class creation."""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.stop_event_loop = True
cls._instance.stop_button_clicked = False
cls._instance.add_run_event_loop_pending_call_thread = None
cls._instance.event_loop_semaphore = QtCore.QSemaphore(EventLoopHelper.MAX_RESOURCES)
cls._instance.stop_event_loop_mutex = QtCore.QMutex(mode=QtCore.QMutex.Recursive)
cls._instance.session_loading = False
return cls._instance
def set_loading_session(self, state):
"""Set a boolean flag if a QtConsole session is being loaded."""
locker = QtCore.QMutexLocker(self.stop_event_loop_mutex)
self.session_loading = state
locker.unlock()
def is_loading_session(self):
"""Return True if a QtConsole session is being loaded."""
locker = QtCore.QMutexLocker(self.stop_event_loop_mutex)
res = self.session_loading
locker.unlock()
return res
def run_event_loop(self, c_ptr):
"""Process Qt events while Python code is running in the PythonNotebook."""
log.debug("1) EventLoopHelper.run_event_loop()")
locker = QtCore.QMutexLocker(self.stop_event_loop_mutex)
log.debug("2) EventLoopHelper.run_event_loop()")
if self.stop_button_clicked or (
flare.main_window() and not flare.main_window().widget().isVisible()
):
log.debug("3) EventLoopHelper.run_event_loop() stop")
self.stop_button_clicked = False
self.session_loading = False
locker.unlock()
self.shutdown_event_loop()
ctypes.pythonapi.PyErr_SetInterrupt()
return 0
if not self.stop_event_loop:
locker.unlock()
log.debug("4) EventLoopHelper.run_event_loop() processEvents()")
QtGui.QApplication.instance().processEvents()
else:
log.debug("5) EventLoopHelper.run_event_loop() stopped running event loop")
self.event_loop_semaphore.release()
return 0
def shutdown_event_loop(self):
"""Stop processing Qt events while Python code is running in the PythonNotebook."""
log.debug("EventLoopHelper.shutdown_event_loop()")