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!""" import os try: # if we're running as a one-click from a MacOS app, # we need to reset the working directory os.chdir(sys._MEIPASS) # @UndefinedVariable except: # if we're not running as a one-click, fail gracefully pass # disable OpenGL in the qt web engine os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu" os.environ["QMLSCENE_DEVICE"] = "softwarecontext" # disable OpenGL on raster surfaces on linux / XCB os.environ["QT_XCB_GL_INTEGRATION"] = "none" from pyface.qt import QtGui, QtCore # enable high DPI scaling QtGui.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) QtGui.QApplication.setHighDpiScaleFactorRoundingPolicy( QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) # use high resolution pixmaps QtGui.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) # 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.qt.check_list_editor # @UnusedImport traitsui.qt.check_list_editor.capitalize = lambda s: s # monkey patch ApplicationWindow to fix toolbar bug from traits.api import observe @observe("tool_bar_managers.items") def _update_tool_bar_managers(self, event): if self.control is not None: # Remove the old toolbars. for child in self.control.children(): if isinstance(child, QtGui.QToolBar): self.control.removeToolBar(child) child.deleteLater() # Add the new toolbars. if event.new is not None: self._create_tool_bar(self.control) from pyface.ui.qt.application_window import ApplicationWindow ApplicationWindow._update_tool_bar_managers = _update_tool_bar_managers # monkey patch pyface.image.image.ImageVolume to stop overwriting the # standard image libraries, which breaks Mac code signing def empty_save(self): pass from pyface.image.image import ImageVolume ImageVolume.save = empty_save # define and install a message handler for Qt errors from traits.api import push_exception_handler # @UnresolvedImport 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, HierarchyPlugin, CategoryPlugin, BinningPlugin, GaussianMixture1DPlugin, GaussianMixture2DPlugin, DensityGatePlugin, BleedthroughLinearPlugin, BeadCalibrationPlugin, AutofluorescencePlugin, ColorTranslationPlugin, TasbePlugin, ChannelStatisticPlugin, MultiChannelStatisticPlugin, TransformStatisticPlugin, RatioPlugin, FlowPeaksPlugin, KMeansPlugin, PCAPlugin, FlowCleanPlugin, tSNEPlugin, SOMPlugin, MSTPlugin as MSTOpPlugin, RegistrationPlugin) from cytoflowgui.view_plugins import (ViewPluginManager, HistogramPlugin, Histogram2DPlugin, ScatterplotPlugin, BarChartPlugin, Stats1DPlugin, Stats2DPlugin, MatrixPlugin, MSTPlugin as MSTViewPlugin, Kde1DPlugin, Kde2DPlugin, ViolinPlotPlugin, TablePlugin, LongTablePlugin, 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(), MatrixPlugin(), MSTViewPlugin(), TablePlugin(), LongTablePlugin(), ExportFCSPlugin()] plugins.extend(view_plugins) op_plugins = [OpPluginManager(), ImportPlugin(), ThresholdPlugin(), RangePlugin(), QuadPlugin(), Range2DPlugin(), PolygonPlugin(), HierarchyPlugin(), CategoryPlugin(), ChannelStatisticPlugin(), MultiChannelStatisticPlugin(), TransformStatisticPlugin(), #MergeStatisticsPlugin(), RatioPlugin(), FlowCleanPlugin(), RegistrationPlugin(), BinningPlugin(), GaussianMixture1DPlugin(), GaussianMixture2DPlugin(), DensityGatePlugin(), KMeansPlugin(), SOMPlugin(), FlowPeaksPlugin(), PCAPlugin(), tSNEPlugin(), MSTOpPlugin(), AutofluorescencePlugin(), BleedthroughLinearPlugin(), BeadCalibrationPlugin(), ColorTranslationPlugin(), TasbePlugin()] plugins.extend(op_plugins) # start the app app = CytoflowApplication(id = '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 (only to) log_q logging.getLogger().handlers.clear() 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) # capture everything sent to stdout in the log too class StreamToLogger(object): """ Fake file-like stream object that redirects writes to a logger instance. """ def __init__(self, logger, level): self.logger = logger self.level = level self.linebuf = '' def write(self, buf): for line in buf.rstrip().splitlines(): self.logger.log(self.level, line.rstrip()) def flush(self): pass sys.stdout = StreamToLogger(logging.getLogger(),logging.INFO) # 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 # @UnresolvedImport 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', force = True) run_gui()