Source code for qtpyvcp.application

"""Main QtPyVCP Application Module

Contains the VCPApplication class with core function and VCP loading logic.
"""
import os
import sys
import imp
import inspect
from pkg_resources import iter_entry_points

from qtpy import API
from qtpy.QtGui import QFontDatabase
from qtpy.QtCore import QTimer, Slot, Qt
from qtpy.QtWidgets import QApplication, QStyleFactory

import qtpyvcp

from qtpyvcp.utilities.logger import initBaseLogger
from qtpyvcp.plugins import getPlugin
from qtpyvcp.widgets.base_widgets.base_widget import QtPyVCPBaseWidget
from qtpyvcp.widgets.form_widgets.main_window import VCPMainWindow

# initialize logging. If a base logger was already initialized in a startup
# script (e.g. vcp_launcher.py), then that logger will be returned, otherwise
# this will initialise a base logger with default log level of DEBUG
LOG = initBaseLogger('qtpyvcp')

# Needed to silence this PySide2 warning:
#    Qt WebEngine seems to be initialized from a plugin. Please set
#    Qt::AA_ShareOpenGLContexts using QCoreApplication::setAttribute
#    before constructing QGuiApplication.
if API == 'pyside2':
    from qtpy.QtCore import Qt
    QApplication.setAttribute(Qt.AA_ShareOpenGLContexts)


[docs]class VCPApplication(QApplication): def __init__(self, theme=None, stylesheet=None, custom_fonts=[]): app_args = (qtpyvcp.OPTIONS.command_line_args or "").split() super(VCPApplication, self).__init__(app_args) opts = qtpyvcp.OPTIONS from qtpyvcp.core import Prefs, Info self.info = Info() self.prefs = Prefs() self.status = getPlugin('status') self.initialiseDataPlugins() theme = opts.theme or theme if theme is not None: self.setStyle(QStyleFactory.create(theme)) stylesheet = opts.stylesheet or stylesheet if stylesheet is not None: self.loadStylesheet(stylesheet) if custom_fonts: for font in custom_fonts: self.loadFont(font) # self.window = self.loadVCPMainWindow(opts, vcp_file) # if self.window is not None: # self.window.show() if opts.hide_cursor: from qtpy.QtGui import QCursor self.setOverrideCursor(QCursor(Qt.BlankCursor)) # Performance monitoring if opts.perfmon: import psutil self.perf = psutil.Process() self.perf_timer = QTimer() self.perf_timer.setInterval(2000) self.perf_timer.timeout.connect(self.logPerformance) self.perf_timer.start() self.aboutToQuit.connect(self.terminate)
[docs] def loadVCPMainWindow(self, opts, vcp_file=None): """ Loads a VCPMainWindow instance defined by a Qt .ui file, a Python .py file, or from a VCP python package. Parameters ---------- vcp_file : str The path or name of the VCP to load. opts : OptDict A OptDict of options to pass to the VCPMainWindow subclass. Returns ------- VCPMainWindow instance """ vcp = opts.vcp or vcp_file if vcp is None: return if os.path.exists(vcp): vcp_path = os.path.realpath(vcp) if os.path.isfile(vcp_path): directory, filename = os.path.split(vcp_path) name, ext = os.path.splitext(filename) if ext == '.ui': LOG.info("Loading VCP from UI file: yellow<{}>".format(vcp)) return VCPMainWindow(opts=opts, ui_file=vcp_path) elif ext == '.py': LOG.info("Loading VCP from PY file: yellow<{}>".format(vcp)) return self.loadPyFile(vcp_path, opts) elif os.path.isdir(vcp_path): LOG.info("VCP is a directory") # TODO: Load from a directory if it has a __main__.py entry point else: try: entry_points = {} for entry_point in iter_entry_points(group='qtpyvcp.example_vcp'): entry_points[entry_point.name] = entry_point for entry_point in iter_entry_points(group='qtpyvcp.vcp'): entry_points[entry_point.name] = entry_point window = entry_points[vcp.lower()].load() return window(opts=opts) except: LOG.exception("Failed to load entry point") LOG.critical("VCP could not be loaded: yellow<{}>".format(vcp)) sys.exit()
[docs] def loadPyFile(self, pyfile, opts): """ Load a .py file, performs some sanity checks to try and determine if the file actually contains a valid VCPMainWindow subclass, and if the checks pass, create and return an instance. This is an internal method, users will usually want to use `loadVCP` instead. Parameters ---------- pyfile : str The path to a .py file to load. opts : OptDict A OptDict of options to pass to the VCPMainWindow subclass. Returns ------- VCPMainWindow instance """ # Add the pyfile module directory to the python path, so that submodules can be loaded module_dir = os.path.dirname(os.path.abspath(pyfile)) sys.path.append(module_dir) # Load the module. It's attributes can be accessed via `python_vcp.attr` module = imp.load_source('python_vcp', pyfile) classes = [obj for name, obj in inspect.getmembers(module) if inspect.isclass(obj) and issubclass(obj, VCPMainWindow) and obj != VCPMainWindow] if len(classes) == 0: raise ValueError("Invalid File Format." " {} has no class inheriting from VCPMainWindow.".format(pyfile)) if len(classes) > 1: LOG.warn("More than one VCPMainWindow class in file yellow<{}>." " The first occurrence (in alphabetical order) will be used: {}" .format(pyfile, classes[0].__name__)) cls = classes[0] # initialize and return the VCPMainWindow subclass return cls(opts=opts)
[docs] def loadStylesheet(self, stylesheet): """Loads a QSS stylesheet file containing styles to be applied to specific Qt and/or QtPyVCP widget classes. Args: stylesheet (str) : Path to the .qss stylesheet to load. """ LOG.info("Loading QSS stylesheet file: yellow<{}>".format(stylesheet)) with open(stylesheet, 'r') as fh: self.setStyleSheet(fh.read())
[docs] def loadFont(self, font_path): """Loads a font file into the font database. The path can specify the location of a font file or a qresource.""" LOG.debug("Loading custom font: %s" % font_path) res = QFontDatabase.addApplicationFont(font_path) if res != 0: LOG.error("Failed to load font: %s", font_path)
@Slot() def logPerformance(self): """Logs total CPU usage (in percent), as well as per-thread usage. """ with self.perf.oneshot(): total_percent = self.perf.cpu_percent(interval=None) total_time = sum(self.perf.cpu_times()) usage = ["{:.3f}".format(total_percent * ((t.system_time + t.user_time) / total_time)) for t in self.perf.threads()] LOG.info("Performance:\n" " Total CPU usage (%): {}\n" " Per Thread: {}\n" .format(total_percent, ' '.join(usage))) def terminate(self): self.terminateWidgets() self.terminateDataPlugins() def initialiseWidgets(self): for w in self.allWidgets(): if isinstance(w, QtPyVCPBaseWidget): w.initialize() def terminateWidgets(self): LOG.debug("Terminating widgets") for w in self.allWidgets(): if isinstance(w, QtPyVCPBaseWidget): try: w.terminate() except Exception: LOG.exception('Error terminating %s widget', w) def initialiseDataPlugins(self): for plugin, obj in qtpyvcp.PLUGINS.items(): LOG.debug("Initializing %s plugin", plugin) obj.initialise() def terminateDataPlugins(self): # terminate in reverse order, this is to prevent problems # when terminating plugins that used other plugins. for plugin, obj in reversed(qtpyvcp.PLUGINS.items()): LOG.debug("Terminating %s plugin", plugin) try: # try so that other plugins are terminated properly # even if one of them fails. obj.terminate() except Exception: LOG.exception('Error terminating %s plugin', plugin)