Source code for cytoflowgui.run

#!/usr/bin/env python3.8
# coding: latin-1
# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2022
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# 
# This program 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 General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
cytoflowgui.run
---------------

The entry-point for the GUI -- sets up and starts the remote process, configures
logging, loads the Envisage plugins, and starts the GUI loop.
"""

try:
    import faulthandler       # @UnresolvedImport
    faulthandler.enable()     # @UndefinedVariable
except:
    # if there's no console, this fails
    pass

import sys, multiprocessing, logging, traceback, threading, argparse

logger = logging.getLogger(__name__)

[docs]def log_notification_handler(_, trait_name, old, new): """ Exception handler for traits notifications """ (exc_type, exc_value, tb) = sys.exc_info() logger.debug('Exception occurred in traits notification ' 'handler for object: %s, trait: %s, old value: %s, ' 'new value: %s.\n%s\n' % ( object, trait_name, old, new, ''.join( traceback.format_exception(exc_type, exc_value, tb) ) ) ) err_string = traceback.format_exception_only(exc_type, exc_value)[0] err_loc = traceback.format_tb(tb)[-1] err_ctx = threading.current_thread().name logging.error("Error: {0}\nLocation: {1}Thread: {2}" \ .format(err_string, err_loc, err_ctx) )
[docs]def log_excepthook(typ, val, tb): """Exception handler for global exceptions""" tb_str = "".join(traceback.format_tb(tb)) logger.debug("Global exception: {0}\n{1}: {2}".format(tb_str, typ, val)) tb_str = traceback.format_tb(tb)[-1] logging.error("Error: {0}: {1}\nLocation: {2}Thread: Main" .format(typ, val, tb_str))
[docs]def run_gui(): """Run the GUI!""" try: # if we're running as a one-click from a MacOS app, # we need to reset the working directory import os; os.chdir(sys._MEIPASS) # @UndefinedVariable except: # if we're not running as a one-click, fail gracefully pass # shut up the warning about Open GL (see below) from pyface.qt import QtGui, QtCore QtGui.QApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) # use high resolution pixmaps QtGui.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) # this is ridiculous, but here's the situation. Qt5 now uses Chromium # as their web renderer. Chromium needs OpenGL. if you don't # initialize OpenGL here, things crash on some platforms. # so now i guess we depend on opengl too. try: from OpenGL import GL # @UnresolvedImport @UnusedImport except ImportError: logger.info("Patching util.find_library for MacOS") from ctypes import util orig_find_library = util.find_library def new_find_library(name): res = orig_find_library(name) if res: return res return '/System/Library/Frameworks/' + name + '.framework/' + name util.find_library = new_find_library from OpenGL import GL util.find_library = orig_find_library # need to import these before a QCoreApplication is instantiated. and that seems # to happen in .... 'import cytoflow' ?? import pyface.qt.QtWebKit # @UnusedImport # take care of the 3 places in the cytoflow module that # need different behavior in a GUI import cytoflow cytoflow.RUNNING_IN_GUI = True # check that we're using the right Qt API from pyface.qt import qt_api cmd_line = " ".join(sys.argv) if qt_api == "pyside": print("Cytoflow uses PyQT; but it is trying to use PySide instead.") print(" - Make sure PyQT is installed.") print(" - If both are installed, and you don't need both, uninstall PySide.") print(" - If you must have both installed, select PyQT by setting the") print(" environment variable QT_API to \"pyqt5\"") print(" * eg, on Linux, type on the command line:") print(" QT_API=\"pyqt5\" " + cmd_line) print(" * on Windows, try: ") print(" setx QT_API \"pyqt5\"") sys.exit(1) # parse args parser = argparse.ArgumentParser(description = 'Cytoflow GUI') parser.add_argument("--debug", action = 'store_true') parser.add_argument("filename", nargs='?', default = "") args = parser.parse_args() # start the remote process remote_process, remote_workflow_connection, remote_canvas_connection, queue_listener = start_remote_process() # getting real tired of the matplotlib deprecation warnings import warnings warnings.filterwarnings('ignore', '.*is deprecated and replaced with.*') # if we're frozen, add _MEIPASS to the pyface search path for icons etc if getattr(sys, 'frozen', False): from pyface.resource_manager import resource_manager resource_manager.extra_paths.append(sys._MEIPASS) # @UndefinedVariable # these three lines stop pkg_resources from trying to load resources # from the __main__ module, which is frozen (and thus not loadable.) from pyface.image_resource import ImageResource icon = ImageResource('icon') icon.search_path = [] # monkey patch the resource manager to use SVGs for icons import pyface.resource.resource_manager pyface.resource.resource_manager.ResourceManager.IMAGE_EXTENSIONS.append('.svg') # monkey patch checklist editor to stop lowercasing import traitsui.qt4.check_list_editor # @UnusedImport traitsui.qt4.check_list_editor.capitalize = lambda s: s # define and install a message handler for Qt errors from traits.api import push_exception_handler def QtMsgHandler(msg_type, msg_context, msg_string): # Convert Qt msg type to logging level log_level = [logging.DEBUG, logging.WARN, logging.ERROR, logging.FATAL] [ int(msg_type) ] logging.log(log_level, 'Qt message: ' + msg_string) from pyface.qt.QtCore import qInstallMessageHandler # @UnresolvedImport qInstallMessageHandler(QtMsgHandler) # install a global (gui) error handler for traits notifications push_exception_handler(handler = log_notification_handler, reraise_exceptions = False, main = True) sys.excepthook = log_excepthook # Import, then load, the envisage plugins from envisage.core_plugin import CorePlugin from envisage.ui.tasks.tasks_plugin import TasksPlugin from cytoflowgui.flow_task import FlowTaskPlugin from cytoflowgui.export_task import ExportFigurePlugin from cytoflowgui.cytoflow_application import CytoflowApplication from cytoflowgui.op_plugins import (OpPluginManager, ImportPlugin, ThresholdPlugin, RangePlugin, QuadPlugin, Range2DPlugin, PolygonPlugin, BinningPlugin, GaussianMixture1DPlugin, GaussianMixture2DPlugin, DensityGatePlugin, BleedthroughLinearPlugin, BeadCalibrationPlugin, AutofluorescencePlugin, ColorTranslationPlugin, TasbePlugin, ChannelStatisticPlugin, TransformStatisticPlugin, RatioPlugin, FlowPeaksPlugin, KMeansPlugin, PCAPlugin) from cytoflowgui.view_plugins import (ViewPluginManager, HistogramPlugin, Histogram2DPlugin, ScatterplotPlugin, BarChartPlugin, Stats1DPlugin, Kde1DPlugin, Kde2DPlugin, ViolinPlotPlugin, TablePlugin, Stats2DPlugin, DensityPlugin, ParallelCoordinatesPlugin, RadvizPlugin, ExportFCSPlugin) plugins = [CorePlugin(), TasksPlugin(), FlowTaskPlugin(), ExportFigurePlugin()] # ordered as we want them to show up in the toolbar view_plugins = [ViewPluginManager(), HistogramPlugin(), ScatterplotPlugin(), Histogram2DPlugin(), DensityPlugin(), Kde1DPlugin(), Kde2DPlugin(), RadvizPlugin(), ParallelCoordinatesPlugin(), ViolinPlotPlugin(), BarChartPlugin(), Stats1DPlugin(), Stats2DPlugin(), TablePlugin(), ExportFCSPlugin()] plugins.extend(view_plugins) op_plugins = [OpPluginManager(), ImportPlugin(), ThresholdPlugin(), RangePlugin(), QuadPlugin(), Range2DPlugin(), PolygonPlugin(), ChannelStatisticPlugin(), TransformStatisticPlugin(), RatioPlugin(), BinningPlugin(), GaussianMixture1DPlugin(), GaussianMixture2DPlugin(), DensityGatePlugin(), KMeansPlugin(), FlowPeaksPlugin(), PCAPlugin(), AutofluorescencePlugin(), BleedthroughLinearPlugin(), BeadCalibrationPlugin(), ColorTranslationPlugin(), TasbePlugin()] plugins.extend(op_plugins) # start the app app = CytoflowApplication(id = 'edu.mit.synbio.cytoflow', plugins = plugins, icon = icon, remote_process = remote_process, remote_workflow_connection = remote_workflow_connection, remote_canvas_connection = remote_canvas_connection, filename = args.filename, debug = args.debug) QtGui.QApplication.instance().setStyle(QtGui.QStyleFactory.create('Fusion')) app.run() remote_process.join() queue_listener.stop() logging.shutdown()
[docs]def monitor_remote_process(proc): """The main method for the (local) thread that monitors the remote process""" proc.join() if proc.exitcode: logging.error("Remote process exited with {}".format(proc.exitcode))
[docs]def start_remote_process(): """ Start the remote process. Creates pipes and synchronization primitives, sets up logging, and starts the remote process and the monitoring thread. """ # communications channels parent_workflow_conn, child_workflow_conn = multiprocessing.Pipe() parent_mpl_conn, child_matplotlib_conn = multiprocessing.Pipe() running_event = multiprocessing.Event() # logging from logging.handlers import QueueListener from cytoflowgui.utility import CallbackHandler log_q = multiprocessing.Queue() def handle(record): logger = logging.getLogger(record.name) if logger.isEnabledFor(record.levelno): logger.handle(record) handler = CallbackHandler(handle) queue_listener = QueueListener(log_q, handler) queue_listener.start() remote_process = multiprocessing.Process(target = remote_main, name = "remote process", args = [parent_workflow_conn, parent_mpl_conn, log_q, running_event]) remote_process.daemon = True remote_process.start() running_event.wait() remote_process_thread = threading.Thread(target = monitor_remote_process, name = "monitor remote process", args = [remote_process]) remote_process_thread.daemon = True remote_process_thread.start() return (remote_process, child_workflow_conn, child_matplotlib_conn, queue_listener)
[docs]def remote_main(parent_workflow_conn, parent_mpl_conn, log_q, running_event): """ The main method for the remote process. Configures logging, sets up the matplotlib backend, and instantiates the `RemoteWorkflow`. """ # this should only ever be main method after a spawn() call # (not fork). So we should have a fresh logger to set up. # messages that end up at the root logger to go log_q from logging.handlers import QueueHandler h = QueueHandler(log_q) logging.getLogger().addHandler(h) # make sure the root logger has a level of DEBUG -- we'll sort out what # to show or not on the local logger logging.getLogger().setLevel(logging.DEBUG) # We want matplotlib to use our backend .... in both the GUI and the # remote process. Must be called BEFORE cytoflow is imported import matplotlib matplotlib.use('module://cytoflowgui.matplotlib_backend_remote') # install a global (gui) error handler for traits notifications from traits.api import push_exception_handler push_exception_handler(handler = log_notification_handler, reraise_exceptions = False, main = True) sys.excepthook = log_excepthook # take care of the 3 places in the cytoflow module that # need different behavior in a GUI import cytoflow cytoflow.RUNNING_IN_GUI = True running_event.set() from cytoflowgui.workflow import RemoteWorkflow RemoteWorkflow().run(parent_workflow_conn, parent_mpl_conn)
if __name__ == '__main__': multiprocessing.freeze_support() multiprocessing.set_start_method('spawn') run_gui()