From 03028ce188092b6ef4846f94420fb2099b10297b Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Wed, 24 Jun 2020 16:44:30 +0200 Subject: [PATCH 01/20] Added dependency on xarray module --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d18e5b3..7dfadf4 100755 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ def find_version(): ], language_level=3, build_dir='build'), python_requires='>=3.6', - install_requires=['typing', 'PyQt5', 'numpy', 'scipy', 'h5py'], + install_requires=['typing', 'PyQt5', 'numpy', 'scipy', 'h5py', 'xarray'], classifiers=[ 'Development Status :: 5 - Production/Stable', From c28ee84914795136f71129ac056ebb548747103b Mon Sep 17 00:00:00 2001 From: Philipp Schmidt Date: Wed, 24 Jun 2020 13:54:18 +0200 Subject: [PATCH 02/20] Move devices module to services package to avoid circular import with mp spawn method --- src/metro/__init__.py | 2 +- src/metro/devices/__init__.py | 1334 ----------------- .../devices/abstract/parallel_operator.py | 5 +- src/metro/services/devices.py | 1334 +++++++++++++++++ 4 files changed, 1336 insertions(+), 1339 deletions(-) mode change 100755 => 100644 src/metro/devices/__init__.py create mode 100755 src/metro/services/devices.py diff --git a/src/metro/__init__.py b/src/metro/__init__.py index ee843b1..5db9234 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -189,7 +189,7 @@ def __getattr__(self, name): 'Measurement': measure.Measurement }) - from metro import devices + from .services import devices globals().update({ 'devices': devices, 'getDevice': devices.get, diff --git a/src/metro/devices/__init__.py b/src/metro/devices/__init__.py old mode 100755 new mode 100644 index 78a682d..e69de29 --- a/src/metro/devices/__init__.py +++ b/src/metro/devices/__init__.py @@ -1,1334 +0,0 @@ - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - - -# This module implements all core functionality for a Metro device. Every -# actual device should inherit from one of the complete implementations -# provided here, CoreDevice (no UI) and WidgetDevice (Qt Widgets). - -# It also possible to inherit from GenericDevice directly, but several -# abstract methods have to implemented in this case. - - -from collections import namedtuple -from functools import partial -from pkg_resources import iter_entry_points -from typing import Optional - -import metro -from metro.services import channels - -QtCore = metro.QtCore -QtWidgets = metro.QtWidgets -QtUic = metro.QtUic - - -_device_entry_points = {ep.name: ep for ep - in iter_entry_points('metro.device')} - -_devices = {} -_morgue = [] - -MeasureOperators = namedtuple('MeasureOperators', - ['point', 'scan', 'trigger', 'limit', 'status']) -_operators = MeasureOperators({}, {}, {}, {}, {}) - -_unconfirmed_kills_counter = 0 - - -def load(ep_name): - """ - Import the given device entry point and return the device class. - - Args: - ep_name (str): Entry point name - - Returns: - Device class object. - """ - - try: - ep = _device_entry_points[ep_name] - cls = ep.load() - except KeyError: - raise ValueError('unknown device entry point') from None - - return cls - - -def create(entry_point, name, parent=None, args=None, state=None): - """ - Create a new device. - - Creates a new device with given name and device class (or path to - module file). - - Args: - name: A string with the name of the new device. - entry_point: Entry point name - parent: Optional parent device object. - args: Optional dict containing arguments to overwrite any of the - default values - state: Optional dict which state informations supplied to the - newly created device. - - Returns: - The newly created device object. - - Raises: - ValueError: Name already in use. - ValueError: Neither device class nor module path given. - Any exception raised by import - """ - - if parent is not None: - name = '{0}!{1}'.format(parent._name, name) - - if name in _devices: - raise ValueError('device with name "{0}" exists already'.format(name)) - - device_class = load(entry_point) - - # Check if the device defines any arguments and replace the default - # values with custom ones given in args, replace lists/tuples by - # their first element as well as remove abstract argument types. - try: - default_args = device_class.arguments - except AttributeError: - default_args = {} - - final_args = {} - - for arg_name, default_value in default_args.items(): - try: - final_value = args[arg_name] - except (KeyError, TypeError): - if (isinstance(default_value, list) or - isinstance(default_value, tuple)): - final_value = default_value[0] - elif isinstance(default_value, metro.AbstractArgument): - final_value = default_value.dialog_bypass() - else: - final_value = default_value - - final_args[arg_name] = final_value - - # Now apply any new value if given - if args is not None: - for k, v in args.items(): - final_args[k] = v - - if state is None: - state = {'visible': True} - - dev = device_class() - - if not dev._prepare(name, parent, final_args, state, entry_point): - raise RuntimeError('device preparation failed') - - if parent is not None: - parent._child_count += 1 - - return dev - - -def killAll(): - """ - Kill all current devices. - """ - - all_parent_devices = [] - - for name in _devices: - if _devices[name]._parent is None: - all_parent_devices.append(_devices[name]) - - for device in all_parent_devices: - device.kill() - - _devices.clear() - - -def checkForLeaks(): - for title in _morgue: - print('WARNING: leak detected of device', - title[:-len(metro.WINDOW_TITLE)-3]) - - -def get(name): - """ - Get a device by name. - - Args: - name: A string with the device name requested. - - Returns: - The device object. - - Raises: - KeyError: Device with given name not found. - """ - - return _devices[name] - - -def getAll(): - """ - Get all loaded devices. - - Returns: - A list with all device objects. - """ - - return _devices.values() - - -def getAvailableName(name): - """ - Get the shortest possible name for a device. - - Based on the supplied name, find the shortest valid name for a - device. In case the name itself is already taken, attach the lowest - possible integer with an underscore. - - Args: - name: String to use as requested name. - - Returns: - Shortest possible valid device name based on supplied string. - """ - - if name not in _devices: - return name - - i = 1 - - while '{0}_{1}'.format(name, i) in _devices: - i = i + 1 - - return '{0}_{1}'.format(name, i) - - -def getDefaultName(entry_point): - return getAvailableName(entry_point[entry_point.rfind('.')+1:]) - - -def findDeviceForChannel(channel_name): - try: - device_name, channel_tag = channel_name.rsplit('#', maxsplit=1) - device = get(device_name) - except ValueError: - # May be thrown by rsplit - return None - except KeyError: - # May be thrown by get - return None - else: - return device - - -def getOperator(type_, name): - if isinstance(type_, str): - return getattr(_operators, type_)[name] - elif isinstance(type_, int): - return _operators[type_][name] - - -def getAllOperators(type_): - if isinstance(type_, str): - return getattr(_operators, type_) - elif isinstance(type_, int): - return _operators[type_] - - -@QtCore.pyqtSlot(QtCore.QObject) -def _on_device_destroyed(obj): - try: - name = obj.windowTitle() - except AttributeError: - pass - else: - _morgue.remove(name) - - -class OperatorThread(QtCore.QThread): - """ - Customized thread to run operators. - - The operator pattern is used throughout Metro to allow a self - contained object controlling an IO-intensive workflow to run in a - separate context such as a thread or process. Typically these - operators contain a "prepare" and a "finalize" method that are - connected to the started and finished signals of a QThread - respectively. - """ - - def __init__(self, parent, operator): - super().__init__(parent) - - operator.moveToThread(self) - self.started.connect(operator.prepare) - self.finished.connect(operator.finalize) - - -class GenericDevice(metro.measure.Node): - """ - Base class for all devices. - - This class contains all the fundamental logic and functionality - required by the framework for a device. A device should not inherit - directly from this class but rather one of the more concrete device - base classes that implement a presentation scheme (for example - CoreDevice or WidgetDevice). The abstract methods in this class are - needed to implement the concept of device visibility: - - setVisible(flag): set the visibility to flag - isVisible: check the visibility - show: always switch the visibilty to shown, equivalent to - setVisible(True) - hide: always switch the visibilty to hidden, equivalent to - setVisible(False) - maximize: bring the window to the foreground if possible - - These are not defined as stub methods, so they can be overwritten by - multiple inheritance. Note that these methods contain the public - frontend interface, not the public user interface. - - There are four stub methods in this class that a concrete device - implementor should overwrite: - - prepare: called whenever a new device is created from this class. - finalize: called when a device is killed. - serialize: called when a device is asked to save its state. - - A device should make no assumptions about its state when restore() - is called! It can either be called after a prepare or at any other - arbitrary point in time. - - All these methods are optional and have empty stubs. There is an - additional method which is implemented as a class method, which is - not guaranteed to be called depending upon the way of device - creation. - - configure: may be called before device creation to initialize the - default arguments. - - Note that due to possibly also extending QObject, this class does - NOT have an __init__ method, and no child class should use one! This - prevents any problems related to different arguments of __init__ - methods of the respective superclasses (QObject vs AbstractDevice). - - If you are extending this class on the framework level - (like GraphicalDevice), also overwrite (and in turn call on the - superclass) the _prepare() method. - """ - - arguments = {} - descriptions = {} - - def _prepare(self, name, parent, args, state, entry_point): - """ - Prepare this device object. - - Note that this method will call the prepare() method on the - device implementation. If you are overwriting this method on the - framework level, you will probably want to initialize your own - state before calling it in this superclass. - - Args: - name: The name for this new device. - parent: Parent device object or None. - args: A dict with startup arguments. - state: A dict with state information. - entry_point: Entry point used to load this device. - """ - - self._name = name - self._parent = parent - self._args = args - self._state = state - self._entry_point = entry_point - - self._child_count = 0 - - self._measuring_slots = [] - self._operators = [] - - _devices[name] = self - - metro.app.deviceCreated(self) - - try: - custom_state = state['custom'] - except KeyError: - custom_state = None - - try: - prepare_result = self.prepare(args, custom_state) - except Exception as e: - prepare_result = e - - if prepare_result is not True and prepare_result is not None: - metro.app.deviceKilled(self) - self._measuring_slots.clear() - del _devices[self._name] - - if prepare_result is False: - return False - else: - raise prepare_result - - if isinstance(self, QtCore.QObject): - self.destroyed.connect(_on_device_destroyed) - - if 'visible' in state: - self.setVisible(state['visible']) - - return True - - def _serialize(self, state): - """ - Serialize this device's state. - - Serializes any private state information of this object and in - turn calls the serialize() method on the device implementation. - - Args: - state: A dict the state should be saved into. - """ - - state['entry_point'] = self._entry_point - state['arguments'] = self._args.copy() - - try: - raw_arguments = self.__class__.arguments - except AttributeError: - pass - else: - for key, value in raw_arguments.items(): - if not isinstance(value, metro.AbstractArgument): - continue - - try: - serialized_value = value.serialize(self._args[key]) - except Exception: - state['arguments'][key] = None - else: - state['arguments'][key] = serialized_value - - state['visible'] = self.isVisible() - - custom_state = self.serialize() - - if custom_state is not None: - state['custom'] = custom_state - - def __str__(self): - return self._name - - def kill(self): - """ - Kill this device. - - Causes this device and all its child devices to be killed. It - will first call this method on all its child devices, then call - the finalize() method on the device implementation and finally - delete it from the global device dict. - """ - - if self._child_count > 0: - children = [] - - for name in _devices: - if _devices[name]._parent == self: - children.append(_devices[name]) - - if self._child_count != len(children): - print('WARNING: Killing device with child_count = {0}, but ' - 'found {1}').format(self._child_count, len(children)) - - for child in children: - child.kill() - - children.clear() - - if self._parent is not None: - self._parent._child_count -= 1 - self._parent = None # Prevents memory leak! - - # Call abstract finalize method - self.finalize() - - if len(self._operators) > 0: - # Make a copy to allow modification of self._operators - ops = self._operators[:] - - for op in ops: - self.measure_removeOperator(*op) - print('WARNING: Removed operator automatically:', op[1]) - - try: - if self.measurement_control_override: - self.measure_releaseControl() - print('WARNING: Released measurement control automatically') - except AttributeError: - pass - - metro.app.deviceKilled(self) - - self._measuring_slots.clear() - - del _devices[self._name] - - try: - name = self.windowTitle() - except AttributeError: - pass - else: - _morgue.append(name) - - def getDeviceName(self): - """ - Get the name of this device. - - Returns: - A string containing the device name. - """ - return self._name - - @staticmethod - def getByName(name): - return get(name) - - @classmethod - def __ge__(cls, other_cls): - if isinstance(other_cls, str): - other_cls = load(other_cls) - - if cls == other_cls: - return True - - for parent_class in cls.__bases__: - if parent_class == metro.GenericDevice: - continue - elif parent_class >= other_cls: - return True - - return False - - def isChildDevice(self): - """ - Query whether this device is a root device or child device - created by another root device. - - Returns: - A boolean indicating whether this device is a child device. - """ - - return self._parent is not None - - def createChildDevice(self, entry_point, name, args=None, state=None): - """ - Create a new child device. - - This method is equivalent to create(entry_point, name, - parent=self, ...). - - Args: - entry_point: Entry point for device. - name: A string with the name for this new child device. This - name will only be attached to the parent's device name. - args: Optional arguments to overwrite any of the default - values. - state: Optional dict which state informations supplied to - the newly created device. - - Returns: - The newly created device object. - - Raises: - Same as create(entry_point, name, parent=self, ...) - """ - - return create(entry_point, name, parent=self, - args=args, state=state) - - def measure_setIndicator(self, key, value): - metro.app.setIndicator('d.{0}.{1}'.format(self._name, key), value) - - def measure_getCurrent(self) -> Optional[metro.Measurement]: - return metro.app.current_meas - - def measure_setCurrent(self, meas: Optional[metro.Measurement]) -> None: - """ - Set the global measurement object. - - When a device overrides the measurement control, it needs to - report the created measurement object while it is active. - - IMPORTANT: The measurement object is assumed to be registered - in the prepared event and STILL registered in the finalized - event. An overriding device should therefore not call this - method in either of these event handlers. The call to register - the object should occur between creating the measurement object - and running it. The best opportunity to unregister it after the - measurement finished is when the StatusOperator switches back - to STANDBY. The controller window will follow this principle - when used as a StatusOperator. - """ - - if not self.measurement_control_override: - raise RuntimeError('device has not overriden measurement control') - - metro.app.current_meas = meas - - def measure_getStorageBase(self) -> Optional[str]: - if metro.app.current_meas is not None: - return metro.app.current_meas.storage_base - - return None - - def measure_connect(self, started=None, stopped=None, prepared=None, - finalized=None): - """ - Connect (actually register) measuring slots for this device. - - A device can register as many slots for each of the available - signals as required and in any combination of calls. The signals - are, in the order they are emitted: - - prepared: when the controller finished preparing the measuring - process and is about to initiate the first step - started: whenever a step starts - stopped: whenever a step stops - finalized: when the complete measuring process is finished or was - aborted by the user. No more started signals will be emitted - at this point. - - All arguments are optional and can be None (their default value) - on any call, the following calls are therefore equivalent: - - self.measure_connect(started=self.myStartedSlot) - self.measure_connect(prepared=self.myPreparedSlot) - - and - - self.measure_connect(started=self.myStartedSlot, - prepared=self.myPreparedSlot) - - Args: - started: Slot to be connected to the started signal. - stopped: Slot to be connected to the stopped signal. - prepared: Slot to be connected to the prepared signal. - finalized: Slot to be connected to the finalized signal. - """ - - self._measuring_slots.append((prepared, started, stopped, finalized)) - - def measure_addOperator(self, type_, tag, op): - """ - Add a measurement operator. - """ - - name = '{0} ({1})'.format(tag, self._name) - op_dict = getattr(_operators, type_) - - if name in op_dict: - raise ValueError('operator tag already in use for this device.') - - op_dict[name] = op - self._operators.append((type_, tag)) - - metro.app.deviceOperatorsChanged() - - def measure_addTaggedOperator(self, type_, tag, op): - """ - Add a measurement operator with callback tags. - - This method is provided when a single operator wants to provide - several tags. Instead of having to construct an object for each - tag by himself, this method will instead construct a proxy - object and adds the tag string to its callback methods as an - arguments. The calling signature hence changes to (e.g. for the - ScanOperator): - - prepareScan(self, tag) - finalizeScan(self, tag) - - Args: - type_: - tag: - op: - """ - if type_ == 'point': - proxy_op = metro.PointOperator() - elif type_ == 'scan': - proxy_op = metro.ScanOperator() - elif type_ == 'trigger': - proxy_op = metro.TriggerOperator() - elif type_ == 'limit': - proxy_op = metro.LimitOperator() - elif type_ == 'status': - proxy_op = metro.StatusOperator - else: - raise ValueError('unknown operator type specified') - - prepare_method = 'prepare{0}'.format(type_.title()) - finalize_method = 'finalize{0}'.format(type_.title()) - - if type_ == 'point': - prepare_method += 's' - finalize_method += 's' - - setattr(proxy_op, prepare_method, - partial(getattr(op, prepare_method), tag)) - setattr(proxy_op, finalize_method, - partial(getattr(op, finalize_method), tag)) - - self.measure_addOperator(type_, tag, proxy_op) - - def measure_removeOperator(self, type_, tag): - name = '{0} ({1})'.format(tag, self._name) - op_dict = getattr(_operators, type_) - - if name not in op_dict: - raise ValueError('no operator with this tag in use for this ' - 'device') - - self._operators.remove((type_, tag)) - del op_dict[name] - - metro.app.deviceOperatorsChanged() - - def measure_overrideControl(self) -> None: - # Do not forget to register the measurement object via - # measure_setCurrent and remove it upon finalization - - metro.app.main_window.overrideMeasurementControl(self._name) - - self.measurement_control_override = True - - def measure_releaseControl(self) -> None: - metro.app.main_window.releaseMeasurementControl() - - self.measurement_control_override = False - - def measure_create(self, point_op, scan_op, trigger_op, limit_op, - status_op=None, scan_count=1, storage_base=None): - cur_channels = [chan for chan - in channels.sortByDependency(channels.getAll()) - if not chan.isStatic()] - - if status_op is None: - status_op = metro.app.main_window - - return metro.Measurement( - list(getAll()), cur_channels, point_op, scan_op, trigger_op, - limit_op, status_op, scan_count, storage_base - ) - - def connectToMeasurement(self, prepared, started, stopped, finalized): - for slots in self._measuring_slots: - if slots[0] is not None: - prepared.connect(slots[0]) - - if slots[1] is not None: - started.connect(slots[1]) - - if slots[2] is not None: - stopped.connect(slots[2]) - - if slots[3] is not None: - finalized.connect(slots[3]) - - def showError(self, text, details=None): - """ - Display an error dialog. - - A wrapper for devices to display an error by the frontend - controller. - - Args: - text: A string describing the error - details: An optional object that provides more details about - the error. May be a string OR an Exception object. In - the latter case, the complete stack trace will be used - in the details. - """ - - metro.app.showError( - 'An error has occured in device {0}:'.format(self._name), - text, details - ) - - def showException(self, e): - """ - Display an error dialog for an exception. - - This call is simply a shortcut for using the exception message - as the error text and the exception itself as details. - - This method is equivalent to - GraphicalDevice.showError(str(e), e). - - Args: - e: The exception to be displayed. - """ - - self.showError(str(e), e) - - # def show(self) - # def hide(self) - # def setVisible(self, flag) - # def isVisible(self) - # def isHidden(self) - # def maximize(self) - - @classmethod - def configure(cls): - """ - Configure the device implementation. - - Stub for the device implementation for when the device class - should be configured prior to creation of the actual device - object. It is not guaranteed that this class method will be - called and this depends on the way of device creation. It should - be used to intialize the default arguments to some sensible - value which can only be determined at runtime. - """ - - pass - - def prepare(self, args, state): - """ - Prepare the device implementation. - - Stub for the device implementation for when a new device is - created from this class. - """ - - pass - - def finalize(self): - """ - Finalize the device implementation. - - Stub for the device implementation for when a device created - from this class is killed. - """ - - pass - - def serialize(self): - """ - Serialize the device implementation's state. - - Stub for the device implementation for when the device should - save its state. - - Returns: - Any python object to be stored as the state for the - implementation of this device or None. - """ - - return None - - -def _searchParentUi(cls): - """ - Search a suitable UI file for a device class. - - Searches the list of parent classes for a suitable .ui file. The - order is determined by the order of appearance in the __bases__ - property of said class object. - - Args: - cls: Class object for the device. - - Returns: - A string containing the UI file to load or None - """ - - for parent in cls.__bases__: - if parent.__name__ == 'Device': - resource_args = ( - parent.__module__, - parent.__module__[parent.__module__.rfind('.')+1:] + '.ui' - ) - - if metro.resource_exists(*resource_args): - return metro.resource_filename(*resource_args) - else: - return _searchParentUi(parent) - - return None - - -class DisplayDevice(GenericDevice): - @staticmethod - def isChannelSupported(channel): - return True - - -class CoreDevice(GenericDevice, QtCore.QObject): - def show(self): - pass - - def hide(self): - pass - - def setVisible(self, flag): - pass - - def isVisible(self): - return False - - def isHidden(self): - return True - - def maximize(self): - pass - - -class TransientDevice(CoreDevice): - pass - - -class WidgetDevice(GenericDevice, QtWidgets.QWidget): - """ - Base class for all graphical Qt devices. - - Any device which wants to show a graphical interface based on Qt - should inherit from this class. It connects the basic device - functionality and lifecycle management with a QWidget. It is highly - recommended that devices to not open further windows that the one - provided by this widget itself. - - This class will try to load a UI file and use this as the prototype - for the QWidget. If the ui_file attribute is not set, it will search - for a .ui file with the same name as the device module itself. If - none is found, this search propagates along the class inheritance. - To disable automatic this behaviour, simply set the ui_file - attribute to None. - - The abstract methods of AbstractDevice are here implemented by - QtWidgets.QWidget. - - Attributes: - ui_file: Optional string containing the UI file to load or None. - """ - - def _prepare(self, name, parent, args, state, entry_point): - """ - Prepare this device object. - - This method extends AbstractDevice by trying to load a UI file - into this QWidget. See the general class description for more - details. - - See AbstractDevice._prepare(name, parent, args, state, - entry_point). - """ - - self._device_group = None - - ui_file = None - - # Either use the attribute or search for ui files with the same - # name as this module - - # FIX USAGE OF ._path - try: - ui_file = self.ui_file - except AttributeError: - res_args = (self.__module__, - f'{self.__module__[self.__module__.rfind(".")+1:]}.ui') - - if metro.resource_exists(*res_args): - ui_file = metro.resource_filename(*res_args) - else: - ui_file = _searchParentUi(self.__class__) - - # uic.loadUi requires a working __str__() for debug messages. - self._name = name - - if ui_file is not None: - QtUic.loadUi(ui_file, self) - self.resize(self.sizeHint()) - - self.setWindowTitle(name) - - if 'geometry' in state and state['geometry'] is not None: - self.setGeometry(*state['geometry']) - - # Now we call _prepare() on our superclass. We delayed this first - # so the UI is already initialized by the time we call prepare() - # of the device implementation. - if not super()._prepare(name, parent, args, state, entry_point): - return False - - return True - - def _serialize(self, state): - """ - Serialize this device's state. - - This method extends AbstractDevice by including the geometry in - the private state information. - - See AbstractDevice._serialize(state). - """ - - geometry = self.geometry() - - state['geometry'] = (geometry.left(), geometry.top(), - geometry.width(), geometry.height()) - - super()._serialize(state) - - def kill(self): - """ - Kill this device. - - This method extends AbstractDevice by also sending a Qt close - event if it has not been triggered by said event. - - See AbstractDevice.kill(). - """ - - # This flag is set by closeEvent() if the unload process was - # initiated by Qt. In this case we do not want to propagate it - # again. - if not hasattr(self, 'close_signaled'): - self.kill_called = True - self.close() - - super().kill() - - if metro.kiosk_mode: - for widget in metro.app.allWidgets(): - if widget.isVisible(): - return - - metro.app.quit() - - def showEvent(self, event): - """ - Qt event handler for show events. - - Called when this QWidget receives a QShowEvent from the window - system and forwards this to the front end controller. Ignored - if the device is in a device group. - - Args: - event: The QShowEvent object belonging to this event - """ - - if self._device_group is not None: - return - - metro.app.deviceShown(self) - - def hideEvent(self, event): - """ - Qt event handler for hide events. - - Called when this QWidget receives a QHideEvent from the window - system and forwards this to the front end controller. Ignored - if the device is in a device group. - - Args: - event: The QHideEvent object belonging to this event - """ - - # In kiosk mode, this event might be triggered after the (last) - # device has already been killed. - if self._name not in _devices: - return - - if self._device_group is not None: - return - - metro.app.deviceHidden(self) - - def closeEvent(self, event): - """ - Qt event handler for close events. - - For child devices, we only hide the window and then ignore the - event. All other devices are killed if the kill process is not - already in progress (and the close event was triggered in - response to it) - - Args: - event: The QCloseEvent object belonging to this event - """ - - if self._device_group is not None: - self._device_group.removeDevice(self) - - # Hide first to emit the hideEvent before the device is - # completely killed. - self.hide() - - if self._parent is not None: - event.ignore() - else: - # This flag is set if close() was called starting from - # kill(), so in this case we do not want to set the - # kill chain in motion ourselves again - if not hasattr(self, 'kill_called'): - self.close_signaled = True - - # This delayed kill fixes the weird SEGFAULTs occuring - # randomly when closing windows. - self.killTimer = metro.QTimer(self) - self.killTimer.timeout.connect(self._delayed_kill) - self.killTimer.setInterval(0) - self.killTimer.setSingleShot(True) - self.killTimer.start() - - @metro.QSlot() - def _delayed_kill(self): - self.kill() - - def maximize(self): - """ - Bring the device interface to the foreground. - """ - - self.showNormal() - self.activateWindow() - - def setWindowTitle(self, new_title): - super().setWindowTitle(f'{new_title} - {metro.WINDOW_TITLE}') - - def createDialog(self, ui_name): - dialog = QtWidgets.QDialog(self) - - ui_file = metro.resource_filename( - self.__module__, - f'{self.__module__[self.__module__.rfind(".")+1:]}_{ui_name}.ui' - ) - - QtUic.loadUi(ui_file, dialog) - dialog.resize(dialog.sizeHint()) - - return dialog - - def setDeviceGroup(self, grp): - self._device_group = grp - - -class DeviceGroup(object): - def __init__(self): - if not isinstance(self, QtWidgets.QWidget): - raise RuntimeError('DeviceGroup must be extended by a QWidget') - - self.menuAdd = QtWidgets.QMenu() - self.menuAdd.aboutToShow.connect(self.on_menuAdd_aboutToShow) - self.menuAdd.triggered.connect(self.on_menuAdd_triggered) - - self.buttonAdd = QtWidgets.QToolButton(self) - self.buttonAdd.setPopupMode(QtWidgets.QToolButton.InstantPopup) - self.buttonAdd.setStyleSheet('QToolButton::menu-indicator ' - '{ image: url(none.jpg); }') - self.buttonAdd.setMenu(self.menuAdd) - self.buttonAdd.setIcon(self.style().standardIcon( - QtWidgets.QStyle.SP_FileDialogNewFolder - )) - - self.show() - - def _getContainedDevices(self): - raise NotImplementedError('_getContainedDevices') - - @metro.QSlot() - def on_menuAdd_aboutToShow(self): - self.menuAdd.clear() - - devs = self._getContainedDevices() - - d_list = sorted(getAll(), key=lambda x: x._name) - for d_obj in d_list: - if isinstance(d_obj, WidgetDevice) and d_obj not in devs: - self.menuAdd.addAction(d_obj._name).setData(d_obj._name) - - # @metro.QSlot(QtWidgets.QAction) - def on_menuAdd_triggered(self, action): - dev = get(action.data()) - - if dev not in self._getContainedDevices(): - self.addDevice(dev) - - def addDevice(self, d): - pass - - def removeDevice(self, d): - pass - - def close(self): - pass - - def dumpGeometry(self): - geometry = self.geometry() - - return (geometry.left(), geometry.top(), - geometry.width(), geometry.height()) - - def restoreGeometry_(self, state): - # TODO: collides with Qt5 symbol - self.setGeometry(QtCore.QRect(*state)) - - def serialize(self): - pass - - -class WindowGroupWidget(QtWidgets.QMdiArea, DeviceGroup): - def __init__(self, title): - super().__init__() - - self.setWindowTitle(f'{title} - {metro.WINDOW_TITLE}') - - self.devices_in_window = [] - - def _getContainedDevices(self): - return self.devices_in_window - - def addDevice(self, d): - if not isinstance(d, QtWidgets.QWidget): - raise TypeError('device must inherit QWidget') - - self.addSubWindow(d) - d.show() - - -class TabGroupWidget(QtWidgets.QTabWidget, DeviceGroup): - class EmptyTabWidget(QtWidgets.QWidget): - def __init__(self, parent): - super().__init__(parent) - - layout = QtWidgets.QHBoxLayout(self) - self.setLayout(layout) - - layout.addItem(QtWidgets.QSpacerItem( - 1, 1, QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Expanding - )) - - self.label = QtWidgets.QLabel( - 'A tab group can contain any number of devices and each added ' - 'device is then accessible by its own tab.

A device ' - 'can be added by clicking the small button in the top right ' - 'corner of this window.

Some devices also provide ' - 'their own method of adding it to a device group, e.g. most ' - 'display devices.' - ) - self.label.setWordWrap(True) - layout.addWidget(self.label) - - layout.addItem(QtWidgets.QSpacerItem( - 1, 1, QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Expanding - )) - - def __init__(self, title, state=None): - super().__init__() - - self.setWindowTitle(f'{title} - {metro.WINDOW_TITLE}') - - self.tabCloseRequested.connect(self.on_tabCloseRequested) - self.setTabsClosable(False) - - self.setCornerWidget(self.buttonAdd, QtCore.Qt.TopRightCorner) - - self.tabEmpty = TabGroupWidget.EmptyTabWidget(self) - self.addTab(self.tabEmpty, '') - - self.devices_in_tabs = [] - - if state is not None: - for dev_name in state[0]: - self.addDevice(get(dev_name)) - - self.setCurrentIndex(state[1]) - - def serialize(self): - return [d._name for d in self.devices_in_tabs], self.currentIndex() - - def closeEvent(self, event): - for d in self.devices_in_tabs.copy(): - self.removeDevice(d) - - metro.app.removeDeviceGroup(self) - - def _getContainedDevices(self): - return self.devices_in_tabs - - def show(self): - super().show() - - # Compute the extra geometry added by the QTabWidget - own_size = self.size() - tab0_size = (self.widget(0).size() if self.count() > 0 - else QtCore.QSize(0, 0)) - - self.extra_height = own_size.height() - tab0_size.height() - self.extra_width = own_size.width() - tab0_size.width() - - def addDevice(self, d): - if not isinstance(d, QtWidgets.QWidget): - raise TypeError('device must inherit QWidget') - - d._TabGroupWidget_orig_geometry = d.geometry() - d._TabGroupWidget_orig_visible = d.isVisible() - - orig_size = d.size() - self.addTab(d, d._name) - - if self.widget(0) == self.tabEmpty: - self.removeTab(0) - self.setTabsClosable(True) - new_size = QtCore.QSize(0, 0) - else: - new_size = self.size() - - if orig_size.width() + self.extra_width > new_size.width(): - new_size.setWidth(orig_size.width() + self.extra_width) - - if orig_size.height() + self.extra_height > new_size.height(): - new_size.setHeight(orig_size.height() + self.extra_height) - - self.resize(new_size) - - d.show() - d.setDeviceGroup(self) # Show first to ensure update - - self.devices_in_tabs.append(d) - self.setCurrentIndex(self.count() - 1) - - def removeDevice(self, d): - index = self.devices_in_tabs.index(d) - - self.removeTab(index) - self.devices_in_tabs.remove(d) - - d.setParent(None) - d.setGeometry(d._TabGroupWidget_orig_geometry) - - d.setDeviceGroup(None) # Show last to ensure update - d.show() - - if not d._TabGroupWidget_orig_visible: - d.hide() - - if self.count() == 0: - self.setTabsClosable(False) - self.addTab(self.tabEmpty, '') - self.resize(self.sizeHint()) - - @metro.QSlot(int) - def on_tabCloseRequested(self, index): - self.removeDevice(self.widget(index)) diff --git a/src/metro/devices/abstract/parallel_operator.py b/src/metro/devices/abstract/parallel_operator.py index e95917e..dc00ef8 100755 --- a/src/metro/devices/abstract/parallel_operator.py +++ b/src/metro/devices/abstract/parallel_operator.py @@ -25,9 +25,6 @@ import traceback import metro -# For some strange reason we need to import this explicitely for the -# second process when using the spawn method for new processes. -from metro.devices import WidgetDevice _targets = {} @@ -187,7 +184,7 @@ def listen(self): self.stepDone.emit() -class Device(WidgetDevice): +class Device(metro.WidgetDevice): def prepare(self, operator_cls, operator_args, newData, state, prefilter=lambda d: d, target=None, target_cap=5, data_pipes=None): diff --git a/src/metro/services/devices.py b/src/metro/services/devices.py new file mode 100755 index 0000000..78a682d --- /dev/null +++ b/src/metro/services/devices.py @@ -0,0 +1,1334 @@ + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + + +# This module implements all core functionality for a Metro device. Every +# actual device should inherit from one of the complete implementations +# provided here, CoreDevice (no UI) and WidgetDevice (Qt Widgets). + +# It also possible to inherit from GenericDevice directly, but several +# abstract methods have to implemented in this case. + + +from collections import namedtuple +from functools import partial +from pkg_resources import iter_entry_points +from typing import Optional + +import metro +from metro.services import channels + +QtCore = metro.QtCore +QtWidgets = metro.QtWidgets +QtUic = metro.QtUic + + +_device_entry_points = {ep.name: ep for ep + in iter_entry_points('metro.device')} + +_devices = {} +_morgue = [] + +MeasureOperators = namedtuple('MeasureOperators', + ['point', 'scan', 'trigger', 'limit', 'status']) +_operators = MeasureOperators({}, {}, {}, {}, {}) + +_unconfirmed_kills_counter = 0 + + +def load(ep_name): + """ + Import the given device entry point and return the device class. + + Args: + ep_name (str): Entry point name + + Returns: + Device class object. + """ + + try: + ep = _device_entry_points[ep_name] + cls = ep.load() + except KeyError: + raise ValueError('unknown device entry point') from None + + return cls + + +def create(entry_point, name, parent=None, args=None, state=None): + """ + Create a new device. + + Creates a new device with given name and device class (or path to + module file). + + Args: + name: A string with the name of the new device. + entry_point: Entry point name + parent: Optional parent device object. + args: Optional dict containing arguments to overwrite any of the + default values + state: Optional dict which state informations supplied to the + newly created device. + + Returns: + The newly created device object. + + Raises: + ValueError: Name already in use. + ValueError: Neither device class nor module path given. + Any exception raised by import + """ + + if parent is not None: + name = '{0}!{1}'.format(parent._name, name) + + if name in _devices: + raise ValueError('device with name "{0}" exists already'.format(name)) + + device_class = load(entry_point) + + # Check if the device defines any arguments and replace the default + # values with custom ones given in args, replace lists/tuples by + # their first element as well as remove abstract argument types. + try: + default_args = device_class.arguments + except AttributeError: + default_args = {} + + final_args = {} + + for arg_name, default_value in default_args.items(): + try: + final_value = args[arg_name] + except (KeyError, TypeError): + if (isinstance(default_value, list) or + isinstance(default_value, tuple)): + final_value = default_value[0] + elif isinstance(default_value, metro.AbstractArgument): + final_value = default_value.dialog_bypass() + else: + final_value = default_value + + final_args[arg_name] = final_value + + # Now apply any new value if given + if args is not None: + for k, v in args.items(): + final_args[k] = v + + if state is None: + state = {'visible': True} + + dev = device_class() + + if not dev._prepare(name, parent, final_args, state, entry_point): + raise RuntimeError('device preparation failed') + + if parent is not None: + parent._child_count += 1 + + return dev + + +def killAll(): + """ + Kill all current devices. + """ + + all_parent_devices = [] + + for name in _devices: + if _devices[name]._parent is None: + all_parent_devices.append(_devices[name]) + + for device in all_parent_devices: + device.kill() + + _devices.clear() + + +def checkForLeaks(): + for title in _morgue: + print('WARNING: leak detected of device', + title[:-len(metro.WINDOW_TITLE)-3]) + + +def get(name): + """ + Get a device by name. + + Args: + name: A string with the device name requested. + + Returns: + The device object. + + Raises: + KeyError: Device with given name not found. + """ + + return _devices[name] + + +def getAll(): + """ + Get all loaded devices. + + Returns: + A list with all device objects. + """ + + return _devices.values() + + +def getAvailableName(name): + """ + Get the shortest possible name for a device. + + Based on the supplied name, find the shortest valid name for a + device. In case the name itself is already taken, attach the lowest + possible integer with an underscore. + + Args: + name: String to use as requested name. + + Returns: + Shortest possible valid device name based on supplied string. + """ + + if name not in _devices: + return name + + i = 1 + + while '{0}_{1}'.format(name, i) in _devices: + i = i + 1 + + return '{0}_{1}'.format(name, i) + + +def getDefaultName(entry_point): + return getAvailableName(entry_point[entry_point.rfind('.')+1:]) + + +def findDeviceForChannel(channel_name): + try: + device_name, channel_tag = channel_name.rsplit('#', maxsplit=1) + device = get(device_name) + except ValueError: + # May be thrown by rsplit + return None + except KeyError: + # May be thrown by get + return None + else: + return device + + +def getOperator(type_, name): + if isinstance(type_, str): + return getattr(_operators, type_)[name] + elif isinstance(type_, int): + return _operators[type_][name] + + +def getAllOperators(type_): + if isinstance(type_, str): + return getattr(_operators, type_) + elif isinstance(type_, int): + return _operators[type_] + + +@QtCore.pyqtSlot(QtCore.QObject) +def _on_device_destroyed(obj): + try: + name = obj.windowTitle() + except AttributeError: + pass + else: + _morgue.remove(name) + + +class OperatorThread(QtCore.QThread): + """ + Customized thread to run operators. + + The operator pattern is used throughout Metro to allow a self + contained object controlling an IO-intensive workflow to run in a + separate context such as a thread or process. Typically these + operators contain a "prepare" and a "finalize" method that are + connected to the started and finished signals of a QThread + respectively. + """ + + def __init__(self, parent, operator): + super().__init__(parent) + + operator.moveToThread(self) + self.started.connect(operator.prepare) + self.finished.connect(operator.finalize) + + +class GenericDevice(metro.measure.Node): + """ + Base class for all devices. + + This class contains all the fundamental logic and functionality + required by the framework for a device. A device should not inherit + directly from this class but rather one of the more concrete device + base classes that implement a presentation scheme (for example + CoreDevice or WidgetDevice). The abstract methods in this class are + needed to implement the concept of device visibility: + + setVisible(flag): set the visibility to flag + isVisible: check the visibility + show: always switch the visibilty to shown, equivalent to + setVisible(True) + hide: always switch the visibilty to hidden, equivalent to + setVisible(False) + maximize: bring the window to the foreground if possible + + These are not defined as stub methods, so they can be overwritten by + multiple inheritance. Note that these methods contain the public + frontend interface, not the public user interface. + + There are four stub methods in this class that a concrete device + implementor should overwrite: + + prepare: called whenever a new device is created from this class. + finalize: called when a device is killed. + serialize: called when a device is asked to save its state. + + A device should make no assumptions about its state when restore() + is called! It can either be called after a prepare or at any other + arbitrary point in time. + + All these methods are optional and have empty stubs. There is an + additional method which is implemented as a class method, which is + not guaranteed to be called depending upon the way of device + creation. + + configure: may be called before device creation to initialize the + default arguments. + + Note that due to possibly also extending QObject, this class does + NOT have an __init__ method, and no child class should use one! This + prevents any problems related to different arguments of __init__ + methods of the respective superclasses (QObject vs AbstractDevice). + + If you are extending this class on the framework level + (like GraphicalDevice), also overwrite (and in turn call on the + superclass) the _prepare() method. + """ + + arguments = {} + descriptions = {} + + def _prepare(self, name, parent, args, state, entry_point): + """ + Prepare this device object. + + Note that this method will call the prepare() method on the + device implementation. If you are overwriting this method on the + framework level, you will probably want to initialize your own + state before calling it in this superclass. + + Args: + name: The name for this new device. + parent: Parent device object or None. + args: A dict with startup arguments. + state: A dict with state information. + entry_point: Entry point used to load this device. + """ + + self._name = name + self._parent = parent + self._args = args + self._state = state + self._entry_point = entry_point + + self._child_count = 0 + + self._measuring_slots = [] + self._operators = [] + + _devices[name] = self + + metro.app.deviceCreated(self) + + try: + custom_state = state['custom'] + except KeyError: + custom_state = None + + try: + prepare_result = self.prepare(args, custom_state) + except Exception as e: + prepare_result = e + + if prepare_result is not True and prepare_result is not None: + metro.app.deviceKilled(self) + self._measuring_slots.clear() + del _devices[self._name] + + if prepare_result is False: + return False + else: + raise prepare_result + + if isinstance(self, QtCore.QObject): + self.destroyed.connect(_on_device_destroyed) + + if 'visible' in state: + self.setVisible(state['visible']) + + return True + + def _serialize(self, state): + """ + Serialize this device's state. + + Serializes any private state information of this object and in + turn calls the serialize() method on the device implementation. + + Args: + state: A dict the state should be saved into. + """ + + state['entry_point'] = self._entry_point + state['arguments'] = self._args.copy() + + try: + raw_arguments = self.__class__.arguments + except AttributeError: + pass + else: + for key, value in raw_arguments.items(): + if not isinstance(value, metro.AbstractArgument): + continue + + try: + serialized_value = value.serialize(self._args[key]) + except Exception: + state['arguments'][key] = None + else: + state['arguments'][key] = serialized_value + + state['visible'] = self.isVisible() + + custom_state = self.serialize() + + if custom_state is not None: + state['custom'] = custom_state + + def __str__(self): + return self._name + + def kill(self): + """ + Kill this device. + + Causes this device and all its child devices to be killed. It + will first call this method on all its child devices, then call + the finalize() method on the device implementation and finally + delete it from the global device dict. + """ + + if self._child_count > 0: + children = [] + + for name in _devices: + if _devices[name]._parent == self: + children.append(_devices[name]) + + if self._child_count != len(children): + print('WARNING: Killing device with child_count = {0}, but ' + 'found {1}').format(self._child_count, len(children)) + + for child in children: + child.kill() + + children.clear() + + if self._parent is not None: + self._parent._child_count -= 1 + self._parent = None # Prevents memory leak! + + # Call abstract finalize method + self.finalize() + + if len(self._operators) > 0: + # Make a copy to allow modification of self._operators + ops = self._operators[:] + + for op in ops: + self.measure_removeOperator(*op) + print('WARNING: Removed operator automatically:', op[1]) + + try: + if self.measurement_control_override: + self.measure_releaseControl() + print('WARNING: Released measurement control automatically') + except AttributeError: + pass + + metro.app.deviceKilled(self) + + self._measuring_slots.clear() + + del _devices[self._name] + + try: + name = self.windowTitle() + except AttributeError: + pass + else: + _morgue.append(name) + + def getDeviceName(self): + """ + Get the name of this device. + + Returns: + A string containing the device name. + """ + return self._name + + @staticmethod + def getByName(name): + return get(name) + + @classmethod + def __ge__(cls, other_cls): + if isinstance(other_cls, str): + other_cls = load(other_cls) + + if cls == other_cls: + return True + + for parent_class in cls.__bases__: + if parent_class == metro.GenericDevice: + continue + elif parent_class >= other_cls: + return True + + return False + + def isChildDevice(self): + """ + Query whether this device is a root device or child device + created by another root device. + + Returns: + A boolean indicating whether this device is a child device. + """ + + return self._parent is not None + + def createChildDevice(self, entry_point, name, args=None, state=None): + """ + Create a new child device. + + This method is equivalent to create(entry_point, name, + parent=self, ...). + + Args: + entry_point: Entry point for device. + name: A string with the name for this new child device. This + name will only be attached to the parent's device name. + args: Optional arguments to overwrite any of the default + values. + state: Optional dict which state informations supplied to + the newly created device. + + Returns: + The newly created device object. + + Raises: + Same as create(entry_point, name, parent=self, ...) + """ + + return create(entry_point, name, parent=self, + args=args, state=state) + + def measure_setIndicator(self, key, value): + metro.app.setIndicator('d.{0}.{1}'.format(self._name, key), value) + + def measure_getCurrent(self) -> Optional[metro.Measurement]: + return metro.app.current_meas + + def measure_setCurrent(self, meas: Optional[metro.Measurement]) -> None: + """ + Set the global measurement object. + + When a device overrides the measurement control, it needs to + report the created measurement object while it is active. + + IMPORTANT: The measurement object is assumed to be registered + in the prepared event and STILL registered in the finalized + event. An overriding device should therefore not call this + method in either of these event handlers. The call to register + the object should occur between creating the measurement object + and running it. The best opportunity to unregister it after the + measurement finished is when the StatusOperator switches back + to STANDBY. The controller window will follow this principle + when used as a StatusOperator. + """ + + if not self.measurement_control_override: + raise RuntimeError('device has not overriden measurement control') + + metro.app.current_meas = meas + + def measure_getStorageBase(self) -> Optional[str]: + if metro.app.current_meas is not None: + return metro.app.current_meas.storage_base + + return None + + def measure_connect(self, started=None, stopped=None, prepared=None, + finalized=None): + """ + Connect (actually register) measuring slots for this device. + + A device can register as many slots for each of the available + signals as required and in any combination of calls. The signals + are, in the order they are emitted: + + prepared: when the controller finished preparing the measuring + process and is about to initiate the first step + started: whenever a step starts + stopped: whenever a step stops + finalized: when the complete measuring process is finished or was + aborted by the user. No more started signals will be emitted + at this point. + + All arguments are optional and can be None (their default value) + on any call, the following calls are therefore equivalent: + + self.measure_connect(started=self.myStartedSlot) + self.measure_connect(prepared=self.myPreparedSlot) + + and + + self.measure_connect(started=self.myStartedSlot, + prepared=self.myPreparedSlot) + + Args: + started: Slot to be connected to the started signal. + stopped: Slot to be connected to the stopped signal. + prepared: Slot to be connected to the prepared signal. + finalized: Slot to be connected to the finalized signal. + """ + + self._measuring_slots.append((prepared, started, stopped, finalized)) + + def measure_addOperator(self, type_, tag, op): + """ + Add a measurement operator. + """ + + name = '{0} ({1})'.format(tag, self._name) + op_dict = getattr(_operators, type_) + + if name in op_dict: + raise ValueError('operator tag already in use for this device.') + + op_dict[name] = op + self._operators.append((type_, tag)) + + metro.app.deviceOperatorsChanged() + + def measure_addTaggedOperator(self, type_, tag, op): + """ + Add a measurement operator with callback tags. + + This method is provided when a single operator wants to provide + several tags. Instead of having to construct an object for each + tag by himself, this method will instead construct a proxy + object and adds the tag string to its callback methods as an + arguments. The calling signature hence changes to (e.g. for the + ScanOperator): + + prepareScan(self, tag) + finalizeScan(self, tag) + + Args: + type_: + tag: + op: + """ + if type_ == 'point': + proxy_op = metro.PointOperator() + elif type_ == 'scan': + proxy_op = metro.ScanOperator() + elif type_ == 'trigger': + proxy_op = metro.TriggerOperator() + elif type_ == 'limit': + proxy_op = metro.LimitOperator() + elif type_ == 'status': + proxy_op = metro.StatusOperator + else: + raise ValueError('unknown operator type specified') + + prepare_method = 'prepare{0}'.format(type_.title()) + finalize_method = 'finalize{0}'.format(type_.title()) + + if type_ == 'point': + prepare_method += 's' + finalize_method += 's' + + setattr(proxy_op, prepare_method, + partial(getattr(op, prepare_method), tag)) + setattr(proxy_op, finalize_method, + partial(getattr(op, finalize_method), tag)) + + self.measure_addOperator(type_, tag, proxy_op) + + def measure_removeOperator(self, type_, tag): + name = '{0} ({1})'.format(tag, self._name) + op_dict = getattr(_operators, type_) + + if name not in op_dict: + raise ValueError('no operator with this tag in use for this ' + 'device') + + self._operators.remove((type_, tag)) + del op_dict[name] + + metro.app.deviceOperatorsChanged() + + def measure_overrideControl(self) -> None: + # Do not forget to register the measurement object via + # measure_setCurrent and remove it upon finalization + + metro.app.main_window.overrideMeasurementControl(self._name) + + self.measurement_control_override = True + + def measure_releaseControl(self) -> None: + metro.app.main_window.releaseMeasurementControl() + + self.measurement_control_override = False + + def measure_create(self, point_op, scan_op, trigger_op, limit_op, + status_op=None, scan_count=1, storage_base=None): + cur_channels = [chan for chan + in channels.sortByDependency(channels.getAll()) + if not chan.isStatic()] + + if status_op is None: + status_op = metro.app.main_window + + return metro.Measurement( + list(getAll()), cur_channels, point_op, scan_op, trigger_op, + limit_op, status_op, scan_count, storage_base + ) + + def connectToMeasurement(self, prepared, started, stopped, finalized): + for slots in self._measuring_slots: + if slots[0] is not None: + prepared.connect(slots[0]) + + if slots[1] is not None: + started.connect(slots[1]) + + if slots[2] is not None: + stopped.connect(slots[2]) + + if slots[3] is not None: + finalized.connect(slots[3]) + + def showError(self, text, details=None): + """ + Display an error dialog. + + A wrapper for devices to display an error by the frontend + controller. + + Args: + text: A string describing the error + details: An optional object that provides more details about + the error. May be a string OR an Exception object. In + the latter case, the complete stack trace will be used + in the details. + """ + + metro.app.showError( + 'An error has occured in device {0}:'.format(self._name), + text, details + ) + + def showException(self, e): + """ + Display an error dialog for an exception. + + This call is simply a shortcut for using the exception message + as the error text and the exception itself as details. + + This method is equivalent to + GraphicalDevice.showError(str(e), e). + + Args: + e: The exception to be displayed. + """ + + self.showError(str(e), e) + + # def show(self) + # def hide(self) + # def setVisible(self, flag) + # def isVisible(self) + # def isHidden(self) + # def maximize(self) + + @classmethod + def configure(cls): + """ + Configure the device implementation. + + Stub for the device implementation for when the device class + should be configured prior to creation of the actual device + object. It is not guaranteed that this class method will be + called and this depends on the way of device creation. It should + be used to intialize the default arguments to some sensible + value which can only be determined at runtime. + """ + + pass + + def prepare(self, args, state): + """ + Prepare the device implementation. + + Stub for the device implementation for when a new device is + created from this class. + """ + + pass + + def finalize(self): + """ + Finalize the device implementation. + + Stub for the device implementation for when a device created + from this class is killed. + """ + + pass + + def serialize(self): + """ + Serialize the device implementation's state. + + Stub for the device implementation for when the device should + save its state. + + Returns: + Any python object to be stored as the state for the + implementation of this device or None. + """ + + return None + + +def _searchParentUi(cls): + """ + Search a suitable UI file for a device class. + + Searches the list of parent classes for a suitable .ui file. The + order is determined by the order of appearance in the __bases__ + property of said class object. + + Args: + cls: Class object for the device. + + Returns: + A string containing the UI file to load or None + """ + + for parent in cls.__bases__: + if parent.__name__ == 'Device': + resource_args = ( + parent.__module__, + parent.__module__[parent.__module__.rfind('.')+1:] + '.ui' + ) + + if metro.resource_exists(*resource_args): + return metro.resource_filename(*resource_args) + else: + return _searchParentUi(parent) + + return None + + +class DisplayDevice(GenericDevice): + @staticmethod + def isChannelSupported(channel): + return True + + +class CoreDevice(GenericDevice, QtCore.QObject): + def show(self): + pass + + def hide(self): + pass + + def setVisible(self, flag): + pass + + def isVisible(self): + return False + + def isHidden(self): + return True + + def maximize(self): + pass + + +class TransientDevice(CoreDevice): + pass + + +class WidgetDevice(GenericDevice, QtWidgets.QWidget): + """ + Base class for all graphical Qt devices. + + Any device which wants to show a graphical interface based on Qt + should inherit from this class. It connects the basic device + functionality and lifecycle management with a QWidget. It is highly + recommended that devices to not open further windows that the one + provided by this widget itself. + + This class will try to load a UI file and use this as the prototype + for the QWidget. If the ui_file attribute is not set, it will search + for a .ui file with the same name as the device module itself. If + none is found, this search propagates along the class inheritance. + To disable automatic this behaviour, simply set the ui_file + attribute to None. + + The abstract methods of AbstractDevice are here implemented by + QtWidgets.QWidget. + + Attributes: + ui_file: Optional string containing the UI file to load or None. + """ + + def _prepare(self, name, parent, args, state, entry_point): + """ + Prepare this device object. + + This method extends AbstractDevice by trying to load a UI file + into this QWidget. See the general class description for more + details. + + See AbstractDevice._prepare(name, parent, args, state, + entry_point). + """ + + self._device_group = None + + ui_file = None + + # Either use the attribute or search for ui files with the same + # name as this module + + # FIX USAGE OF ._path + try: + ui_file = self.ui_file + except AttributeError: + res_args = (self.__module__, + f'{self.__module__[self.__module__.rfind(".")+1:]}.ui') + + if metro.resource_exists(*res_args): + ui_file = metro.resource_filename(*res_args) + else: + ui_file = _searchParentUi(self.__class__) + + # uic.loadUi requires a working __str__() for debug messages. + self._name = name + + if ui_file is not None: + QtUic.loadUi(ui_file, self) + self.resize(self.sizeHint()) + + self.setWindowTitle(name) + + if 'geometry' in state and state['geometry'] is not None: + self.setGeometry(*state['geometry']) + + # Now we call _prepare() on our superclass. We delayed this first + # so the UI is already initialized by the time we call prepare() + # of the device implementation. + if not super()._prepare(name, parent, args, state, entry_point): + return False + + return True + + def _serialize(self, state): + """ + Serialize this device's state. + + This method extends AbstractDevice by including the geometry in + the private state information. + + See AbstractDevice._serialize(state). + """ + + geometry = self.geometry() + + state['geometry'] = (geometry.left(), geometry.top(), + geometry.width(), geometry.height()) + + super()._serialize(state) + + def kill(self): + """ + Kill this device. + + This method extends AbstractDevice by also sending a Qt close + event if it has not been triggered by said event. + + See AbstractDevice.kill(). + """ + + # This flag is set by closeEvent() if the unload process was + # initiated by Qt. In this case we do not want to propagate it + # again. + if not hasattr(self, 'close_signaled'): + self.kill_called = True + self.close() + + super().kill() + + if metro.kiosk_mode: + for widget in metro.app.allWidgets(): + if widget.isVisible(): + return + + metro.app.quit() + + def showEvent(self, event): + """ + Qt event handler for show events. + + Called when this QWidget receives a QShowEvent from the window + system and forwards this to the front end controller. Ignored + if the device is in a device group. + + Args: + event: The QShowEvent object belonging to this event + """ + + if self._device_group is not None: + return + + metro.app.deviceShown(self) + + def hideEvent(self, event): + """ + Qt event handler for hide events. + + Called when this QWidget receives a QHideEvent from the window + system and forwards this to the front end controller. Ignored + if the device is in a device group. + + Args: + event: The QHideEvent object belonging to this event + """ + + # In kiosk mode, this event might be triggered after the (last) + # device has already been killed. + if self._name not in _devices: + return + + if self._device_group is not None: + return + + metro.app.deviceHidden(self) + + def closeEvent(self, event): + """ + Qt event handler for close events. + + For child devices, we only hide the window and then ignore the + event. All other devices are killed if the kill process is not + already in progress (and the close event was triggered in + response to it) + + Args: + event: The QCloseEvent object belonging to this event + """ + + if self._device_group is not None: + self._device_group.removeDevice(self) + + # Hide first to emit the hideEvent before the device is + # completely killed. + self.hide() + + if self._parent is not None: + event.ignore() + else: + # This flag is set if close() was called starting from + # kill(), so in this case we do not want to set the + # kill chain in motion ourselves again + if not hasattr(self, 'kill_called'): + self.close_signaled = True + + # This delayed kill fixes the weird SEGFAULTs occuring + # randomly when closing windows. + self.killTimer = metro.QTimer(self) + self.killTimer.timeout.connect(self._delayed_kill) + self.killTimer.setInterval(0) + self.killTimer.setSingleShot(True) + self.killTimer.start() + + @metro.QSlot() + def _delayed_kill(self): + self.kill() + + def maximize(self): + """ + Bring the device interface to the foreground. + """ + + self.showNormal() + self.activateWindow() + + def setWindowTitle(self, new_title): + super().setWindowTitle(f'{new_title} - {metro.WINDOW_TITLE}') + + def createDialog(self, ui_name): + dialog = QtWidgets.QDialog(self) + + ui_file = metro.resource_filename( + self.__module__, + f'{self.__module__[self.__module__.rfind(".")+1:]}_{ui_name}.ui' + ) + + QtUic.loadUi(ui_file, dialog) + dialog.resize(dialog.sizeHint()) + + return dialog + + def setDeviceGroup(self, grp): + self._device_group = grp + + +class DeviceGroup(object): + def __init__(self): + if not isinstance(self, QtWidgets.QWidget): + raise RuntimeError('DeviceGroup must be extended by a QWidget') + + self.menuAdd = QtWidgets.QMenu() + self.menuAdd.aboutToShow.connect(self.on_menuAdd_aboutToShow) + self.menuAdd.triggered.connect(self.on_menuAdd_triggered) + + self.buttonAdd = QtWidgets.QToolButton(self) + self.buttonAdd.setPopupMode(QtWidgets.QToolButton.InstantPopup) + self.buttonAdd.setStyleSheet('QToolButton::menu-indicator ' + '{ image: url(none.jpg); }') + self.buttonAdd.setMenu(self.menuAdd) + self.buttonAdd.setIcon(self.style().standardIcon( + QtWidgets.QStyle.SP_FileDialogNewFolder + )) + + self.show() + + def _getContainedDevices(self): + raise NotImplementedError('_getContainedDevices') + + @metro.QSlot() + def on_menuAdd_aboutToShow(self): + self.menuAdd.clear() + + devs = self._getContainedDevices() + + d_list = sorted(getAll(), key=lambda x: x._name) + for d_obj in d_list: + if isinstance(d_obj, WidgetDevice) and d_obj not in devs: + self.menuAdd.addAction(d_obj._name).setData(d_obj._name) + + # @metro.QSlot(QtWidgets.QAction) + def on_menuAdd_triggered(self, action): + dev = get(action.data()) + + if dev not in self._getContainedDevices(): + self.addDevice(dev) + + def addDevice(self, d): + pass + + def removeDevice(self, d): + pass + + def close(self): + pass + + def dumpGeometry(self): + geometry = self.geometry() + + return (geometry.left(), geometry.top(), + geometry.width(), geometry.height()) + + def restoreGeometry_(self, state): + # TODO: collides with Qt5 symbol + self.setGeometry(QtCore.QRect(*state)) + + def serialize(self): + pass + + +class WindowGroupWidget(QtWidgets.QMdiArea, DeviceGroup): + def __init__(self, title): + super().__init__() + + self.setWindowTitle(f'{title} - {metro.WINDOW_TITLE}') + + self.devices_in_window = [] + + def _getContainedDevices(self): + return self.devices_in_window + + def addDevice(self, d): + if not isinstance(d, QtWidgets.QWidget): + raise TypeError('device must inherit QWidget') + + self.addSubWindow(d) + d.show() + + +class TabGroupWidget(QtWidgets.QTabWidget, DeviceGroup): + class EmptyTabWidget(QtWidgets.QWidget): + def __init__(self, parent): + super().__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + self.setLayout(layout) + + layout.addItem(QtWidgets.QSpacerItem( + 1, 1, QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.Expanding + )) + + self.label = QtWidgets.QLabel( + 'A tab group can contain any number of devices and each added ' + 'device is then accessible by its own tab.

A device ' + 'can be added by clicking the small button in the top right ' + 'corner of this window.

Some devices also provide ' + 'their own method of adding it to a device group, e.g. most ' + 'display devices.' + ) + self.label.setWordWrap(True) + layout.addWidget(self.label) + + layout.addItem(QtWidgets.QSpacerItem( + 1, 1, QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.Expanding + )) + + def __init__(self, title, state=None): + super().__init__() + + self.setWindowTitle(f'{title} - {metro.WINDOW_TITLE}') + + self.tabCloseRequested.connect(self.on_tabCloseRequested) + self.setTabsClosable(False) + + self.setCornerWidget(self.buttonAdd, QtCore.Qt.TopRightCorner) + + self.tabEmpty = TabGroupWidget.EmptyTabWidget(self) + self.addTab(self.tabEmpty, '') + + self.devices_in_tabs = [] + + if state is not None: + for dev_name in state[0]: + self.addDevice(get(dev_name)) + + self.setCurrentIndex(state[1]) + + def serialize(self): + return [d._name for d in self.devices_in_tabs], self.currentIndex() + + def closeEvent(self, event): + for d in self.devices_in_tabs.copy(): + self.removeDevice(d) + + metro.app.removeDeviceGroup(self) + + def _getContainedDevices(self): + return self.devices_in_tabs + + def show(self): + super().show() + + # Compute the extra geometry added by the QTabWidget + own_size = self.size() + tab0_size = (self.widget(0).size() if self.count() > 0 + else QtCore.QSize(0, 0)) + + self.extra_height = own_size.height() - tab0_size.height() + self.extra_width = own_size.width() - tab0_size.width() + + def addDevice(self, d): + if not isinstance(d, QtWidgets.QWidget): + raise TypeError('device must inherit QWidget') + + d._TabGroupWidget_orig_geometry = d.geometry() + d._TabGroupWidget_orig_visible = d.isVisible() + + orig_size = d.size() + self.addTab(d, d._name) + + if self.widget(0) == self.tabEmpty: + self.removeTab(0) + self.setTabsClosable(True) + new_size = QtCore.QSize(0, 0) + else: + new_size = self.size() + + if orig_size.width() + self.extra_width > new_size.width(): + new_size.setWidth(orig_size.width() + self.extra_width) + + if orig_size.height() + self.extra_height > new_size.height(): + new_size.setHeight(orig_size.height() + self.extra_height) + + self.resize(new_size) + + d.show() + d.setDeviceGroup(self) # Show first to ensure update + + self.devices_in_tabs.append(d) + self.setCurrentIndex(self.count() - 1) + + def removeDevice(self, d): + index = self.devices_in_tabs.index(d) + + self.removeTab(index) + self.devices_in_tabs.remove(d) + + d.setParent(None) + d.setGeometry(d._TabGroupWidget_orig_geometry) + + d.setDeviceGroup(None) # Show last to ensure update + d.show() + + if not d._TabGroupWidget_orig_visible: + d.hide() + + if self.count() == 0: + self.setTabsClosable(False) + self.addTab(self.tabEmpty, '') + self.resize(self.sizeHint()) + + @metro.QSlot(int) + def on_tabCloseRequested(self, index): + self.removeDevice(self.widget(index)) From 9b283f522a793517e5140ab12a7f2898a1215a8d Mon Sep 17 00:00:00 2001 From: Philipp Schmidt Date: Mon, 6 Jul 2020 14:48:39 +0200 Subject: [PATCH 03/20] Add metro.init_mp_support to fix missing initialization with spawn method --- src/metro/__init__.py | 15 +++++++++++++++ src/metro/devices/abstract/parallel_operator.py | 1 + 2 files changed, 16 insertions(+) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 5db9234..112878b 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -217,6 +217,21 @@ def __getattr__(self, name): }) +def init_mp_support(): + try: + core_mode + except NameError: + pass + else: + return + + class _Args: + core_mode = False + kiosk_mode = False + + init(_Args, 'Metro') + + def start(prog_name='metro', window_title='Metro', cli_hook=None): args, argv_left = parse_args(prog_name, cli_hook=cli_hook) init(args, window_title) diff --git a/src/metro/devices/abstract/parallel_operator.py b/src/metro/devices/abstract/parallel_operator.py index dc00ef8..d558d8e 100755 --- a/src/metro/devices/abstract/parallel_operator.py +++ b/src/metro/devices/abstract/parallel_operator.py @@ -25,6 +25,7 @@ import traceback import metro +metro.init_mp_support() _targets = {} From 0a8b66152c2751e498fc243f43dc47bd25c6afa8 Mon Sep 17 00:00:00 2001 From: Philipp Schmidt Date: Mon, 6 Jul 2020 14:52:04 +0200 Subject: [PATCH 04/20] No longer import metro.services.devices into the metro namespace as a module, but add all public symbols --- src/metro/__init__.py | 13 +++++++-- src/metro/devices/display/hist2d.py | 2 +- src/metro/frontend/application.py | 35 ++++++++++++------------ src/metro/frontend/controller.py | 38 ++++++++++++-------------- src/metro/frontend/dialogs/measure.py | 3 +- src/metro/frontend/dialogs/profiles.py | 4 +-- src/metro/frontend/dialogs/storage.py | 7 ++--- 7 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 112878b..970a1aa 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -191,17 +191,26 @@ def __getattr__(self, name): from .services import devices globals().update({ - 'devices': devices, + 'loadDevice': devices.load, + 'createDevice': devices.create, 'getDevice': devices.get, 'getAllDevices': devices.getAll, 'getOperator': devices.getOperator, 'getAllOperators': devices.getAllOperators, + 'killAllDevices': devices.killAll, + 'getAvailableDeviceName': devices.getAvailableName, + 'getDefaultDeviceName': devices.getDefaultName, + 'findDeviceForChannel': devices.findDeviceForChannel, + 'checkForDeviceLeaks': devices.checkForLeaks, 'OperatorThread': devices.OperatorThread, 'GenericDevice': devices.GenericDevice, 'DisplayDevice': devices.DisplayDevice, 'CoreDevice': devices.CoreDevice, 'TransientDevice': devices.TransientDevice, - 'WidgetDevice': devices.WidgetDevice + 'WidgetDevice': devices.WidgetDevice, + 'DeviceGroup': devices.DeviceGroup, + 'WindowGroupWidget': devices.WindowGroupWidget, + 'TabGroupWidget': devices.TabGroupWidget }) from .frontend import arguments diff --git a/src/metro/devices/display/hist2d.py b/src/metro/devices/display/hist2d.py index 6acb7f3..59e5468 100755 --- a/src/metro/devices/display/hist2d.py +++ b/src/metro/devices/display/hist2d.py @@ -1469,7 +1469,7 @@ def prepare(self, args, state): else: self.ch_in = args['channel'] - ch_dev = metro.devices.findDeviceForChannel(self.ch_in.name) + ch_dev = metro.findDeviceForChannel(self.ch_in.name) if ch_dev is not None and ch_dev >= 'project.generic2d': self.proj_dev = ch_dev diff --git a/src/metro/frontend/application.py b/src/metro/frontend/application.py index 1dd34f0..06fa711 100644 --- a/src/metro/frontend/application.py +++ b/src/metro/frontend/application.py @@ -15,7 +15,6 @@ import metro from metro.services import profiles -from metro import devices if not metro.core_mode: from metro.frontend import controller @@ -146,7 +145,7 @@ def _loadDevice(self, entry_point, device_class, name, state): args.update(abstract_args) try: - devices.create(entry_point, name, args=args, state=state) + metro.createDevice(entry_point, name, args=args, state=state) except Exception as e: self.showException('An error occured on creating a device:', e) @@ -417,7 +416,7 @@ def loadProfile(self, path): try: device_classes[value['entry_point']] = \ - devices.load(value['entry_point']) + metro.loadDevice(value['entry_point']) except KeyError: if 'path' in value or 'module' in value: return self.showError( @@ -504,7 +503,7 @@ def saveProfile(self, path, device_list=None, channel_list=None, device_list = [d.getDeviceName() for d in metro.getAllDevices()] for name in device_list: - device = devices.get(name) + device = metro.getDevice(name) if device._parent is not None: continue @@ -589,10 +588,10 @@ def setIndicator(self, key, value): else: self.indicators[key] = value - def addDeviceGroup(self, dev_grp: devices.DeviceGroup): + def addDeviceGroup(self, dev_grp: metro.DeviceGroup): self.device_groups.append(dev_grp) - def removeDeviceGroup(self, dev_grp: devices.DeviceGroup): + def removeDeviceGroup(self, dev_grp: metro.DeviceGroup): self.device_groups.remove(dev_grp) @staticmethod @@ -676,7 +675,7 @@ def _getGeometryHash(self): return geometry_hash.hexdigest() def _loadDevice(self, entry_point, device_class, name, state): - if issubclass(device_class, devices.WidgetDevice): + if issubclass(device_class, metro.WidgetDevice): raise NotImplementedError('WidgetDevice not supported in core ' 'mode') @@ -703,7 +702,7 @@ def editScriptedChannel(self, channel=None): def createNewDevice(self, entry_point, name=None, args={}): try: - device_class = devices.load(entry_point) + device_class = metro.loadDevice(entry_point) except ImportError as e: self.showError('An error occured when loading the device entry ' 'point "{}":'.format(entry_point), @@ -728,7 +727,7 @@ def createNewDevice(self, entry_point, name=None, args={}): return if name is None: - name = devices.getDefaultName(entry_point) + name = metro.getDefaultDeviceName(entry_point) if not issubclass(device_class, metro.TransientDevice): dialog = dialogs.NewDeviceDialog(name, device_class, args) @@ -744,7 +743,7 @@ def createNewDevice(self, entry_point, name=None, args={}): final_args = {} try: - d = devices.create(final_name, entry_point, args=final_args) + d = metro.createDevice(final_name, entry_point, args=final_args) except Exception as e: self.showException('An error occured on constructing device ' '"{0}":'.format(final_name), e) @@ -953,7 +952,7 @@ def editScriptedChannel(self, channel=None): def createNewDevice(self, entry_point, name=None, args={}): try: - device_class = devices.load(entry_point) + device_class = metro.loadDevice(entry_point) except ImportError as e: self.showError('An error occured when loading the device entry ' 'point "{}":'.format(entry_point), @@ -978,7 +977,7 @@ def createNewDevice(self, entry_point, name=None, args={}): return if name is None: - name = devices.getDefaultName(entry_point) + name = metro.getDefaultDeviceName(entry_point) if not issubclass(device_class, metro.TransientDevice): dialog = dialogs.NewDeviceDialog(name, device_class, args) @@ -994,7 +993,7 @@ def createNewDevice(self, entry_point, name=None, args={}): final_args = {} try: - d = devices.create(entry_point, final_name, args=final_args) + d = metro.createDevice(entry_point, final_name, args=final_args) except Exception as e: self.showException('An error occured on constructing device ' '"{0}":'.format(final_name), e) @@ -1027,7 +1026,7 @@ def createDisplayDevice(self, channel, entry_point=None, show_dialog=False, ) return - device_class = devices.load(entry_point) + device_class = metro.loadDevice(entry_point) try: if not device_class.isChannelSupported(channel): @@ -1037,7 +1036,7 @@ def createDisplayDevice(self, channel, entry_point=None, show_dialog=False, 'device:', str(e)) return None - device_name = devices.getAvailableName('{0}[{1}]'.format( + device_name = metro.getAvailableDeviceName('{0}[{1}]'.format( channel_name, entry_point[entry_point.rfind('.')+1:]) ) @@ -1053,8 +1052,8 @@ def createDisplayDevice(self, channel, entry_point=None, show_dialog=False, display_device = self.createNewDevice(entry_point, device_name, final_args) else: - display_device = devices.create(entry_point, device_name, - args=final_args) + display_device = metro.createDevice(entry_point, device_name, + args=final_args) return display_device @@ -1087,7 +1086,7 @@ def screenshot(self, base_path, devices_list=None): # to keep track of this state in the application object. if devices_list is None: - devices_list = devices.getAll() + devices_list = metro.getAllDevices() controller_pixmap = self.main_window.grab() regular_device_pixmaps = [] diff --git a/src/metro/frontend/controller.py b/src/metro/frontend/controller.py index 5bde54e..faf367a 100755 --- a/src/metro/frontend/controller.py +++ b/src/metro/frontend/controller.py @@ -11,12 +11,8 @@ from pkg_resources import iter_entry_points import metro -from metro import devices -from metro.services import channels -from metro.services import measure -from metro.services import profiles -from metro.frontend import dialogs -from metro.frontend import widgets +from metro.services import channels, measure, profiles +from metro.frontend import dialogs, widgets QtCore = metro.QtCore QtGui = metro.QtGui @@ -604,15 +600,15 @@ def changeEvent(self, event): ) if was_minimized: - for d in devices.getAll(): + for d in metro.getAllDevices(): if d.isVisible(): d.maximize() def deviceCreated(self, device): - if isinstance(device, devices.TransientDevice): + if isinstance(device, metro.TransientDevice): return - elif isinstance(device, devices.DisplayDevice) or \ - isinstance(device._parent, devices.DisplayDevice): + elif isinstance(device, metro.DisplayDevice) or \ + isinstance(device._parent, metro.DisplayDevice): layout = self.layoutDisplayDevices visible = self.actionShowDisplayDevices.isChecked() else: @@ -659,7 +655,7 @@ def deviceKilled(self, device): n_channels = len(channels_label) - if isinstance(device, devices.DisplayDevice): + if isinstance(device, metro.DisplayDevice): layout = self.layoutDisplayDevices else: layout = self.layoutDevices @@ -681,7 +677,7 @@ def deviceKilled(self, device): def deviceShown(self, device): # Skip display devices - if isinstance(device, devices.DisplayDevice): + if isinstance(device, metro.DisplayDevice): return device_name = device._name @@ -690,7 +686,7 @@ def deviceShown(self, device): def deviceHidden(self, device): # Skip display devices - if isinstance(device, devices.DisplayDevice): + if isinstance(device, metro.DisplayDevice): return device_name = device._name @@ -703,7 +699,7 @@ def deviceOperatorsChanged(self): self.selectLinearScanOperator.clear() self.selectLinearScanOperator.addItem('none') - for name in sorted(list(devices._operators.scan.keys())): + for name in sorted(list(metro.getAllOperators('scan').keys())): self.selectLinearScanOperator.addItem(name) idx = self.selectLinearScanOperator.findText(prev_op, @@ -714,7 +710,7 @@ def deviceOperatorsChanged(self): def channelOpened(self, channel): name = channel.name - device = devices.findDeviceForChannel(name) + device = metro.findDeviceForChannel(name) if device is not None: try: @@ -737,7 +733,7 @@ def channelOpened(self, channel): # Callback from channels module (implemented as a normal Watcher) def channelClosed(self, channel): - device = devices.findDeviceForChannel(channel.name) + device = metro.findDeviceForChannel(channel.name) if device is not None: try: @@ -771,7 +767,7 @@ def quit(self): for dev_grp in metro.app.device_groups: dev_grp.close() - devices.killAll() + metro.killAllDevices() metro.app.processEvents() metro.app.quit() @@ -785,7 +781,7 @@ def quit(self): @QtCore.pyqtSlot() def _leak_check(self): - devices.checkForLeaks() + metro.checkForDeviceLeaks() @QtCore.pyqtSlot(int) def updateStatus(self, code): @@ -1038,7 +1034,7 @@ def on_buttonRun_pressed(self): self._resetStepDurationGuess() - cur_nodes = list(devices.getAll()) + cur_nodes = list(metro.getAllDevices()) cur_nodes.append(self) cur_channels = [chan for chan @@ -1260,7 +1256,7 @@ def on_menuNewDevice_triggered(self, action): if not confirmed or not text: return - window_group = devices.WindowGroupWidget(text) + window_group = metro.WindowGroupWidget(text) metro.app.addDeviceGroup(window_group) elif name == '__group_by_tab__': @@ -1271,7 +1267,7 @@ def on_menuNewDevice_triggered(self, action): if not confirmed or not text: return - tab_group = devices.TabGroupWidget(text) + tab_group = metro.TabGroupWidget(text) metro.app.addDeviceGroup(tab_group) elif action == self.actionShowDisplayDevices: diff --git a/src/metro/frontend/dialogs/measure.py b/src/metro/frontend/dialogs/measure.py index 0d6ad44..d4e2163 100644 --- a/src/metro/frontend/dialogs/measure.py +++ b/src/metro/frontend/dialogs/measure.py @@ -11,7 +11,6 @@ from PyQt5 import uic as QtUic import metro -from metro import devices from metro.services import measure from metro.frontend import arguments @@ -112,7 +111,7 @@ def getMenu(self): if op_name == self.op_name: new_action.setChecked(True) - ops = getattr(devices._operators, self.op_key) + ops = metro.getAllOperators(self.op_key) if len(self.builtins) > 0 and len(ops) > 0: self.menu.addSeparator() diff --git a/src/metro/frontend/dialogs/profiles.py b/src/metro/frontend/dialogs/profiles.py index cf8a138..d89b4aa 100755 --- a/src/metro/frontend/dialogs/profiles.py +++ b/src/metro/frontend/dialogs/profiles.py @@ -11,7 +11,6 @@ from PyQt5 import uic as QtUic import metro -from metro import devices class SaveProfileDialog(QtWidgets.QDialog): @@ -28,7 +27,8 @@ def __init__(self, profiles): self.editName.lineEdit().setText('') - device_list = sorted(devices.getAll(), key=lambda x: x.getDeviceName()) + device_list = sorted(metro.getAllDevices(), + key=lambda x: x.getDeviceName()) for device in device_list: device_item = QtWidgets.QListWidgetItem(device.getDeviceName(), diff --git a/src/metro/frontend/dialogs/storage.py b/src/metro/frontend/dialogs/storage.py index 567289c..200b5cb 100755 --- a/src/metro/frontend/dialogs/storage.py +++ b/src/metro/frontend/dialogs/storage.py @@ -15,7 +15,6 @@ from PyQt5 import uic as QtUic import metro -from metro import devices class ReplayStreamChannelDialog(QtWidgets.QDialog): @@ -274,9 +273,9 @@ def on_loader_finished(self): if not p[3].isChecked(): continue - devices.create(p[1], '{0}_{1}'.format(name, p[0]), - args={'channel': chan, 'count_rows': False}, - state={'visible': False, 'custom': p[2]}) + metro.createDevice(p[1], '{0}_{1}'.format(name, p[0]), + args={'channel': chan, 'count_rows': False}, + state={'visible': False, 'custom': p[2]}) self.progress_timer.stop() self.labelProgress.setText('Done!') From 941fb5704e282c56dad90c4ced8405de221768f1 Mon Sep 17 00:00:00 2001 From: Philipp Schmidt Date: Mon, 6 Jul 2020 14:52:40 +0200 Subject: [PATCH 05/20] Rename GenericDevice.__ge__ to isSubDevice and simplify the implementation --- src/metro/devices/display/hist2d.py | 2 +- src/metro/services/devices.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/metro/devices/display/hist2d.py b/src/metro/devices/display/hist2d.py index 59e5468..b717899 100755 --- a/src/metro/devices/display/hist2d.py +++ b/src/metro/devices/display/hist2d.py @@ -1471,7 +1471,7 @@ def prepare(self, args, state): ch_dev = metro.findDeviceForChannel(self.ch_in.name) - if ch_dev is not None and ch_dev >= 'project.generic2d': + if ch_dev is not None and ch_dev.isSubDevice('project.generic2d'): self.proj_dev = ch_dev self.dirty = False diff --git a/src/metro/services/devices.py b/src/metro/services/devices.py index 78a682d..5c18678 100755 --- a/src/metro/services/devices.py +++ b/src/metro/services/devices.py @@ -503,20 +503,11 @@ def getByName(name): return get(name) @classmethod - def __ge__(cls, other_cls): + def isSubDevice(cls, other_cls): if isinstance(other_cls, str): other_cls = load(other_cls) - if cls == other_cls: - return True - - for parent_class in cls.__bases__: - if parent_class == metro.GenericDevice: - continue - elif parent_class >= other_cls: - return True - - return False + return issubclass(cls, other_cls) def isChildDevice(self): """ From 4c1b712790c6b31fbaff8c14e8bb1e5ceaa923be Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Thu, 9 Jul 2020 19:35:49 +0200 Subject: [PATCH 06/20] Restructures the initialization routines in the metro module for clearer importing --- src/metro/__init__.py | 292 ++++++++++-------- .../devices/abstract/parallel_operator.py | 3 +- 2 files changed, 158 insertions(+), 137 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 970a1aa..05825cc 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -18,7 +18,7 @@ def parse_args(prog_name, cli_hook=None): cli_actions['profile'] = cli.add_argument( '--profile', dest='profile', action='store', type=str, - help='load the given profile (either the name without .json ' + help='Load the given profile (either the name without .json ' 'relative to the profile directory or complete path) on ' 'startup.' ) @@ -27,26 +27,26 @@ def parse_args(prog_name, cli_hook=None): cli_actions['kiosk'] = mode_group.add_argument( '--kiosk', dest='kiosk_mode', action='store_true', - help='start in kiosk mode with hidden controller window and a ' + help='Start in kiosk mode with hidden controller window and a ' 'single visible device controlling the application state.' ) cli_actions['core'] = mode_group.add_argument( '--core', dest='core_mode', action='store_true', - help='start in core mode without a graphical interface and no ' - 'dependency on QtGui or QtWidgets' + help='Start in core mode without a graphical interface and no ' + 'dependency on QtGui or QtWidgets.' ) dev_group = cli.add_argument_group(title='development flags') cli_actions['experimental'] = dev_group.add_argument( '--experimental', dest='experimental', action='store_true', - help='turns on various experimental features.' + help='Turns on various experimental features.' ) cli_actions['gc-debug'] = dev_group.add_argument( '--gc-debug', dest='gc_debug', action='store', type=int, default=0, - help='specify the debug level for the python garbage collector.' + help='Specify the debug level for the Python garbage collector.' ) if cli_hook is not None: @@ -55,40 +55,18 @@ def parse_args(prog_name, cli_hook=None): return cli.parse_known_args() -def init(args, window_title, local_path='~/.metro', - profile_path='~/.metro/profiles'): - import sys - - if args.core_mode: - def die(msg): - print('Fatal error during initialization: ' + msg) - sys.exit(0) - else: - def die(msg): - if sys.version_info[0] == 2: - import Tkinter # different name in python2 - tkinter = Tkinter - else: - import tkinter - - root = tkinter.Tk() - root.wm_title(window_title) - - frame = tkinter.Frame(borderwidth=5) - - label = tkinter.Label(frame, justify=tkinter.LEFT, wraplength=450, - text='Fatal error during ' - 'initialization:\n\n' + msg) - label.grid(padx=5, pady=5) - - button = tkinter.Button(frame, text='Close', - command=lambda: root.quit()) - button.grid(pady=5) +def init_core(): + try: + if not globals()['load_GUI']: + return # already initialized in core mode + except KeyError: + globals()['load_GUI'] = False - frame.grid() - frame.mainloop() + import sys - sys.exit(0) + def die(msg): + print('Fatal error during initialization: ' + msg) + sys.exit(0) if sys.version_info[:2] < (3, 3): die('Requires python version >= 3.3 (found {0})'.format( @@ -99,10 +77,6 @@ def die(msg): import typing # noqa (F401) import numpy # noqa (F401) from PyQt5 import QtCore # noqa (F401) - - if not args.core_mode: - from PyQt5 import QtGui # noqa (F401) - from PyQt5 import QtWidgets # noqa (F401) except ImportError as e: die('An essential dependency ({0}) could not be imported and is ' 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) @@ -112,145 +86,193 @@ def die(msg): # constructed module objects to allow the definition of related # classes without any actual dependency. - import os - import pkg_resources - - local_path = os.path.expanduser(local_path) - os.makedirs(local_path, exist_ok=True) - - profile_path = os.path.expanduser(profile_path) - os.makedirs(profile_path, exist_ok=True) - globals().update({ - 'WINDOW_TITLE': window_title, - 'LOCAL_PATH': local_path, - 'PROFILE_PATH': profile_path, - 'resource_exists': pkg_resources.resource_exists, - 'resource_filename': pkg_resources.resource_filename, - 'die': die - }) - - globals().update({ - 'core_mode': args.core_mode, - 'kiosk_mode': args.kiosk_mode, - - 'QtCore': QtCore, - 'QObject': QtCore.QObject, - 'QSignal': QtCore.pyqtSignal, - 'QSlot': QtCore.pyqtSlot, + 'QtCore': QtCore, + 'QObject': QtCore.QObject, + 'QSignal': QtCore.pyqtSignal, + 'QSlot': QtCore.pyqtSlot, 'QProperty': QtCore.pyqtProperty, - 'QTimer': QtCore.QTimer, - 'QThread': QtCore.QThread, - 'QConsts': QtCore.Qt, + 'QTimer': QtCore.QTimer, + 'QThread': QtCore.QThread, + 'QConsts': QtCore.Qt }) - if args.core_mode: + if not globals()['load_GUI']: class EmptyQtModule: def __getattr__(self, name): return QtCore.QObject - - QtGui = EmptyQtModule() # noqa + + QtGui = EmptyQtModule() # noqa QtWidgets = EmptyQtModule() # noqa - QtUic = EmptyQtModule() - - else: - from PyQt5 import uic as QtUic - - globals().update({ - 'QtGui': QtGui, - 'QtWidgets': QtWidgets, - 'QtUic': QtUic - }) + QtUic = EmptyQtModule() + + globals().update({ + 'QtGui': QtGui, + 'QtWidgets': QtWidgets, + 'QtUic': QtUic, + }) from .services import channels globals().update({ - 'channels': channels, - 'getChannel': channels.get, - 'getAllChannels': channels.getAll, - 'queryChannels': channels.query, + 'channels': channels, + 'getChannel': channels.get, + 'getAllChannels': channels.getAll, + 'queryChannels': channels.query, 'AbstractChannel': channels.AbstractChannel, - 'ChannelAdapter': channels.ChannelAdapter, - 'StreamChannel': channels.StreamChannel, - 'NumericChannel': channels.NumericChannel, + 'ChannelAdapter': channels.ChannelAdapter, + 'StreamChannel': channels.StreamChannel, + 'NumericChannel': channels.NumericChannel, 'DatagramChannel': channels.DatagramChannel, - 'LogChannel': channels.LogChannel + 'LogChannel': channels.LogChannel }) from .services import measure globals().update({ - 'measure': measure, - 'RunBlock': measure.RunBlock, - 'StepBlock': measure.StepBlock, - 'BlockListener': measure.BlockListener, - 'ScanOperator': measure.ScanOperator, + 'measure': measure, + 'RunBlock': measure.RunBlock, + 'StepBlock': measure.StepBlock, + 'BlockListener': measure.BlockListener, + 'ScanOperator': measure.ScanOperator, 'TriggerOperator': measure.TriggerOperator, - 'LimitOperator': measure.LimitOperator, - 'StatusOperator': measure.StatusOperator, - 'Measurement': measure.Measurement + 'LimitOperator': measure.LimitOperator, + 'StatusOperator': measure.StatusOperator, + 'Measurement': measure.Measurement }) from .services import devices globals().update({ - 'loadDevice': devices.load, - 'createDevice': devices.create, - 'getDevice': devices.get, - 'getAllDevices': devices.getAll, - 'getOperator': devices.getOperator, - 'getAllOperators': devices.getAllOperators, - 'killAllDevices': devices.killAll, + 'loadDevice': devices.load, + 'createDevice': devices.create, + 'getDevice': devices.get, + 'getAllDevices': devices.getAll, + 'getOperator': devices.getOperator, + 'getAllOperators': devices.getAllOperators, + 'killAllDevices': devices.killAll, 'getAvailableDeviceName': devices.getAvailableName, - 'getDefaultDeviceName': devices.getDefaultName, - 'findDeviceForChannel': devices.findDeviceForChannel, - 'checkForDeviceLeaks': devices.checkForLeaks, - 'OperatorThread': devices.OperatorThread, - 'GenericDevice': devices.GenericDevice, - 'DisplayDevice': devices.DisplayDevice, - 'CoreDevice': devices.CoreDevice, - 'TransientDevice': devices.TransientDevice, - 'WidgetDevice': devices.WidgetDevice, - 'DeviceGroup': devices.DeviceGroup, - 'WindowGroupWidget': devices.WindowGroupWidget, - 'TabGroupWidget': devices.TabGroupWidget + 'getDefaultDeviceName': devices.getDefaultName, + 'findDeviceForChannel': devices.findDeviceForChannel, + 'checkForDeviceLeaks': devices.checkForLeaks, + 'OperatorThread': devices.OperatorThread, + 'GenericDevice': devices.GenericDevice, + 'DisplayDevice': devices.DisplayDevice, + 'CoreDevice': devices.CoreDevice, + 'TransientDevice': devices.TransientDevice, + 'WidgetDevice': devices.WidgetDevice, + 'DeviceGroup': devices.DeviceGroup, + 'WindowGroupWidget': devices.WindowGroupWidget, + 'TabGroupWidget': devices.TabGroupWidget }) from .frontend import arguments globals().update({ - 'arguments': arguments, + 'arguments': arguments, 'AbstractArgument': arguments.AbstractArgument, - 'IndexArgument': arguments.IndexArgument, + 'IndexArgument': arguments.IndexArgument, 'ComboBoxArgument': arguments.ComboBoxArgument, - 'DeviceArgument': arguments.DeviceArgument, - 'ChannelArgument': arguments.ChannelArgument, + 'DeviceArgument': arguments.DeviceArgument, + 'ChannelArgument': arguments.ChannelArgument, 'OperatorArgument': arguments.OperatorArgument, - 'FileArgument': arguments.FileArgument + 'FileArgument': arguments.FileArgument }) -def init_mp_support(): +def init_gui(): try: - core_mode - except NameError: - pass - else: - return + if globals()['load_GUI']: + return # already initialized these + except KeyError: + globals()['load_GUI'] = True + + try: + from PyQt5 import QtGui # noqa (F401) + from PyQt5 import QtWidgets # noqa (F401) + from PyQt5 import uic as QtUic + except ImportError as e: + die('An essential dependency ({0}) could not be imported and is ' + 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) + + import sys + + def die(msg): + if sys.version_info[0] == 2: + import Tkinter # different name in python2 + tkinter = Tkinter + else: + import tkinter + + root = tkinter.Tk() + root.wm_title(window_title) + + frame = tkinter.Frame(borderwidth=5) + + label = tkinter.Label(frame, justify=tkinter.LEFT, wraplength=450, + text='Fatal error during ' + 'initialization:\n\n' + msg) + label.grid(padx=5, pady=5) + + button = tkinter.Button(frame, text='Close', + command=lambda: root.quit()) + button.grid(pady=5) + + frame.grid() + frame.mainloop() + + sys.exit(0) + + globals().update({ + 'QtGui': QtGui, + 'QtWidgets': QtWidgets, + 'QtUic': QtUic, + 'die': die + }) - class _Args: - core_mode = False - kiosk_mode = False - init(_Args, 'Metro') +def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', + local_path='~/.metro', profile_path='~/.metro/profiles'): + import os + import pkg_resources + + local_path = os.path.expanduser(local_path) + os.makedirs(local_path, exist_ok=True) + + profile_path = os.path.expanduser(profile_path) + os.makedirs(profile_path, exist_ok=True) + + globals().update({ + 'WINDOW_TITLE': window_title, + 'LOCAL_PATH': local_path, + 'PROFILE_PATH': profile_path, + 'resource_exists': pkg_resources.resource_exists, + 'resource_filename': pkg_resources.resource_filename, + 'core_mode': core_mode, + 'kiosk_mode': kiosk_mode + }) + + # Initialize GUI modules if not in core mode + if not core_mode: + init_gui() + + # Initialize the core modules + init_core() def start(prog_name='metro', window_title='Metro', cli_hook=None): args, argv_left = parse_args(prog_name, cli_hook=cli_hook) - init(args, window_title) + init_metro(args.core_mode, args.kiosk_mode, window_title) from .frontend import application - if core_mode: # noqa + if core_mode: # noqa app_class = application.CoreApplication else: app_class = application.GuiApplication app = app_class(args, argv_left) app.exec_() + + +# Initialize metro per default in core mode, unless it is imported by the +# installed metro app (check for call via setuptools entry_point). +# import inspect +# if "load_entry_point('metro-sci'" not in inspect.stack()[-1][-2][0]: +# init_core() diff --git a/src/metro/devices/abstract/parallel_operator.py b/src/metro/devices/abstract/parallel_operator.py index d558d8e..7b79ef9 100755 --- a/src/metro/devices/abstract/parallel_operator.py +++ b/src/metro/devices/abstract/parallel_operator.py @@ -25,8 +25,7 @@ import traceback import metro -metro.init_mp_support() - +metro.init_metro() _targets = {} Target = collections.namedtuple('Target', ['name', 'process', 'active', From 65acd1719843aff2baee588a7cc85049e33c6ad3 Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Fri, 10 Jul 2020 11:55:03 +0200 Subject: [PATCH 07/20] Change global flag load_GUI --- src/metro/__init__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 05825cc..16651a8 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -54,13 +54,14 @@ def parse_args(prog_name, cli_hook=None): return cli.parse_known_args() +load_GUI = None def init_core(): - try: - if not globals()['load_GUI']: - return # already initialized in core mode - except KeyError: - globals()['load_GUI'] = False + global load_GUI + if load_GUI is None: + load_GUI = False # initialize in core mode + elif not load_GUI: + return # already initialized in core mode import sys @@ -97,7 +98,7 @@ def die(msg): 'QConsts': QtCore.Qt }) - if not globals()['load_GUI']: + if not load_GUI: class EmptyQtModule: def __getattr__(self, name): return QtCore.QObject @@ -177,11 +178,11 @@ def __getattr__(self, name): def init_gui(): - try: - if globals()['load_GUI']: - return # already initialized these - except KeyError: - globals()['load_GUI'] = True + global load_GUI + if load_GUI is None: + load_GUI = True # initialize GUI modules + elif load_GUI: + return # already initialized GUI modules try: from PyQt5 import QtGui # noqa (F401) From 1df2a3133acf9f2b93d5dd17b78b7610e699045a Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Fri, 10 Jul 2020 14:11:17 +0200 Subject: [PATCH 08/20] Fix Python version checking and 'die' routine initialization --- src/metro/__init__.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 16651a8..5aa4c10 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -65,12 +65,14 @@ def init_core(): import sys - def die(msg): - print('Fatal error during initialization: ' + msg) - sys.exit(0) + if not load_GUI: + def die(msg): + print('Fatal error during initialization: ' + msg) + sys.exit(0) + globals().update({'die': die}) if sys.version_info[:2] < (3, 3): - die('Requires python version >= 3.3 (found {0})'.format( + globals()['die']('Requires python version >= 3.3 (found {0})'.format( sys.version[:sys.version.find(' ')] )) @@ -110,7 +112,7 @@ def __getattr__(self, name): globals().update({ 'QtGui': QtGui, 'QtWidgets': QtWidgets, - 'QtUic': QtUic, + 'QtUic': QtUic }) from .services import channels @@ -184,16 +186,6 @@ def init_gui(): elif load_GUI: return # already initialized GUI modules - try: - from PyQt5 import QtGui # noqa (F401) - from PyQt5 import QtWidgets # noqa (F401) - from PyQt5 import uic as QtUic - except ImportError as e: - die('An essential dependency ({0}) could not be imported and is ' - 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) - - import sys - def die(msg): if sys.version_info[0] == 2: import Tkinter # different name in python2 @@ -202,6 +194,10 @@ def die(msg): import tkinter root = tkinter.Tk() + try: + window_title = globals()['WINDOW_TITLE'] + except KeyError: + window_title = 'Metro' root.wm_title(window_title) frame = tkinter.Frame(borderwidth=5) @@ -220,6 +216,16 @@ def die(msg): sys.exit(0) + try: + from PyQt5 import QtGui # noqa (F401) + from PyQt5 import QtWidgets # noqa (F401) + from PyQt5 import uic as QtUic + except ImportError as e: + die('An essential dependency ({0}) could not be imported and is ' + 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) + + import sys + globals().update({ 'QtGui': QtGui, 'QtWidgets': QtWidgets, From a39c1b2890d0dff9893965832fd69aaf0a97dd3c Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Fri, 10 Jul 2020 14:13:14 +0200 Subject: [PATCH 09/20] Remove obsolete comments --- src/metro/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 5aa4c10..b52ed37 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -276,10 +276,3 @@ def start(prog_name='metro', window_title='Metro', cli_hook=None): app = app_class(args, argv_left) app.exec_() - - -# Initialize metro per default in core mode, unless it is imported by the -# installed metro app (check for call via setuptools entry_point). -# import inspect -# if "load_entry_point('metro-sci'" not in inspect.stack()[-1][-2][0]: -# init_core() From 18dae0ca77368eb1a9cd8b3df04bbb3b4315e628 Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Mon, 13 Jul 2020 17:24:46 +0200 Subject: [PATCH 10/20] Style adjustments for flake8 and consistence --- src/metro/__init__.py | 34 ++++++++++++++++-------------- src/metro/devices/display/image.py | 2 +- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index b52ed37..528abe1 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -18,35 +18,35 @@ def parse_args(prog_name, cli_hook=None): cli_actions['profile'] = cli.add_argument( '--profile', dest='profile', action='store', type=str, - help='Load the given profile (either the name without .json ' + help='load the given profile (either the name without .json ' 'relative to the profile directory or complete path) on ' - 'startup.' + 'startup' ) mode_group = cli.add_mutually_exclusive_group() cli_actions['kiosk'] = mode_group.add_argument( '--kiosk', dest='kiosk_mode', action='store_true', - help='Start in kiosk mode with hidden controller window and a ' - 'single visible device controlling the application state.' + help='start in kiosk mode with hidden controller window and a ' + 'single visible device controlling the application state' ) cli_actions['core'] = mode_group.add_argument( '--core', dest='core_mode', action='store_true', - help='Start in core mode without a graphical interface and no ' - 'dependency on QtGui or QtWidgets.' + help='start in core mode without a graphical interface and no ' + 'dependency on QtGui or QtWidgets' ) dev_group = cli.add_argument_group(title='development flags') cli_actions['experimental'] = dev_group.add_argument( '--experimental', dest='experimental', action='store_true', - help='Turns on various experimental features.' + help='turns on various experimental features' ) cli_actions['gc-debug'] = dev_group.add_argument( '--gc-debug', dest='gc_debug', action='store', type=int, default=0, - help='Specify the debug level for the Python garbage collector.' + help='specify the debug level for the Python garbage collector' ) if cli_hook is not None: @@ -54,14 +54,16 @@ def parse_args(prog_name, cli_hook=None): return cli.parse_known_args() + load_GUI = None + def init_core(): global load_GUI if load_GUI is None: - load_GUI = False # initialize in core mode + load_GUI = False # initialize in core mode elif not load_GUI: - return # already initialized in core mode + return # already initialized in core mode import sys @@ -104,11 +106,11 @@ def die(msg): class EmptyQtModule: def __getattr__(self, name): return QtCore.QObject - + QtGui = EmptyQtModule() # noqa QtWidgets = EmptyQtModule() # noqa - QtUic = EmptyQtModule() - + QtUic = EmptyQtModule() # noqa + globals().update({ 'QtGui': QtGui, 'QtWidgets': QtWidgets, @@ -182,9 +184,9 @@ def __getattr__(self, name): def init_gui(): global load_GUI if load_GUI is None: - load_GUI = True # initialize GUI modules + load_GUI = True # initialize GUI modules elif load_GUI: - return # already initialized GUI modules + return # already initialized GUI modules def die(msg): if sys.version_info[0] == 2: @@ -255,7 +257,7 @@ def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', 'kiosk_mode': kiosk_mode }) - # Initialize GUI modules if not in core mode + # Initialize GUI modules if not in core mode if not core_mode: init_gui() diff --git a/src/metro/devices/display/image.py b/src/metro/devices/display/image.py index 665c9ad..5001f33 100644 --- a/src/metro/devices/display/image.py +++ b/src/metro/devices/display/image.py @@ -67,7 +67,7 @@ def paint(self, p, *args): else self.image.shape[:2][::-1] if self._coords is None: - p.drawImage(metro.QtCore.QRectF(0,0,*shape), self.qimage) + p.drawImage(metro.QtCore.QRectF(0, 0, *shape), self.qimage) else: p.drawImage(metro.QtCore.QRectF(*self._coords), self.qimage) From 0b8181e86f1cd58cc0a9d6b7d8787715aa3ebd51 Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Thu, 9 Jul 2020 19:35:49 +0200 Subject: [PATCH 11/20] Restructures the initialization routines in the metro module for clearer importing --- src/metro/__init__.py | 292 ++++++++++-------- .../devices/abstract/parallel_operator.py | 3 +- 2 files changed, 158 insertions(+), 137 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 970a1aa..05825cc 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -18,7 +18,7 @@ def parse_args(prog_name, cli_hook=None): cli_actions['profile'] = cli.add_argument( '--profile', dest='profile', action='store', type=str, - help='load the given profile (either the name without .json ' + help='Load the given profile (either the name without .json ' 'relative to the profile directory or complete path) on ' 'startup.' ) @@ -27,26 +27,26 @@ def parse_args(prog_name, cli_hook=None): cli_actions['kiosk'] = mode_group.add_argument( '--kiosk', dest='kiosk_mode', action='store_true', - help='start in kiosk mode with hidden controller window and a ' + help='Start in kiosk mode with hidden controller window and a ' 'single visible device controlling the application state.' ) cli_actions['core'] = mode_group.add_argument( '--core', dest='core_mode', action='store_true', - help='start in core mode without a graphical interface and no ' - 'dependency on QtGui or QtWidgets' + help='Start in core mode without a graphical interface and no ' + 'dependency on QtGui or QtWidgets.' ) dev_group = cli.add_argument_group(title='development flags') cli_actions['experimental'] = dev_group.add_argument( '--experimental', dest='experimental', action='store_true', - help='turns on various experimental features.' + help='Turns on various experimental features.' ) cli_actions['gc-debug'] = dev_group.add_argument( '--gc-debug', dest='gc_debug', action='store', type=int, default=0, - help='specify the debug level for the python garbage collector.' + help='Specify the debug level for the Python garbage collector.' ) if cli_hook is not None: @@ -55,40 +55,18 @@ def parse_args(prog_name, cli_hook=None): return cli.parse_known_args() -def init(args, window_title, local_path='~/.metro', - profile_path='~/.metro/profiles'): - import sys - - if args.core_mode: - def die(msg): - print('Fatal error during initialization: ' + msg) - sys.exit(0) - else: - def die(msg): - if sys.version_info[0] == 2: - import Tkinter # different name in python2 - tkinter = Tkinter - else: - import tkinter - - root = tkinter.Tk() - root.wm_title(window_title) - - frame = tkinter.Frame(borderwidth=5) - - label = tkinter.Label(frame, justify=tkinter.LEFT, wraplength=450, - text='Fatal error during ' - 'initialization:\n\n' + msg) - label.grid(padx=5, pady=5) - - button = tkinter.Button(frame, text='Close', - command=lambda: root.quit()) - button.grid(pady=5) +def init_core(): + try: + if not globals()['load_GUI']: + return # already initialized in core mode + except KeyError: + globals()['load_GUI'] = False - frame.grid() - frame.mainloop() + import sys - sys.exit(0) + def die(msg): + print('Fatal error during initialization: ' + msg) + sys.exit(0) if sys.version_info[:2] < (3, 3): die('Requires python version >= 3.3 (found {0})'.format( @@ -99,10 +77,6 @@ def die(msg): import typing # noqa (F401) import numpy # noqa (F401) from PyQt5 import QtCore # noqa (F401) - - if not args.core_mode: - from PyQt5 import QtGui # noqa (F401) - from PyQt5 import QtWidgets # noqa (F401) except ImportError as e: die('An essential dependency ({0}) could not be imported and is ' 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) @@ -112,145 +86,193 @@ def die(msg): # constructed module objects to allow the definition of related # classes without any actual dependency. - import os - import pkg_resources - - local_path = os.path.expanduser(local_path) - os.makedirs(local_path, exist_ok=True) - - profile_path = os.path.expanduser(profile_path) - os.makedirs(profile_path, exist_ok=True) - globals().update({ - 'WINDOW_TITLE': window_title, - 'LOCAL_PATH': local_path, - 'PROFILE_PATH': profile_path, - 'resource_exists': pkg_resources.resource_exists, - 'resource_filename': pkg_resources.resource_filename, - 'die': die - }) - - globals().update({ - 'core_mode': args.core_mode, - 'kiosk_mode': args.kiosk_mode, - - 'QtCore': QtCore, - 'QObject': QtCore.QObject, - 'QSignal': QtCore.pyqtSignal, - 'QSlot': QtCore.pyqtSlot, + 'QtCore': QtCore, + 'QObject': QtCore.QObject, + 'QSignal': QtCore.pyqtSignal, + 'QSlot': QtCore.pyqtSlot, 'QProperty': QtCore.pyqtProperty, - 'QTimer': QtCore.QTimer, - 'QThread': QtCore.QThread, - 'QConsts': QtCore.Qt, + 'QTimer': QtCore.QTimer, + 'QThread': QtCore.QThread, + 'QConsts': QtCore.Qt }) - if args.core_mode: + if not globals()['load_GUI']: class EmptyQtModule: def __getattr__(self, name): return QtCore.QObject - - QtGui = EmptyQtModule() # noqa + + QtGui = EmptyQtModule() # noqa QtWidgets = EmptyQtModule() # noqa - QtUic = EmptyQtModule() - - else: - from PyQt5 import uic as QtUic - - globals().update({ - 'QtGui': QtGui, - 'QtWidgets': QtWidgets, - 'QtUic': QtUic - }) + QtUic = EmptyQtModule() + + globals().update({ + 'QtGui': QtGui, + 'QtWidgets': QtWidgets, + 'QtUic': QtUic, + }) from .services import channels globals().update({ - 'channels': channels, - 'getChannel': channels.get, - 'getAllChannels': channels.getAll, - 'queryChannels': channels.query, + 'channels': channels, + 'getChannel': channels.get, + 'getAllChannels': channels.getAll, + 'queryChannels': channels.query, 'AbstractChannel': channels.AbstractChannel, - 'ChannelAdapter': channels.ChannelAdapter, - 'StreamChannel': channels.StreamChannel, - 'NumericChannel': channels.NumericChannel, + 'ChannelAdapter': channels.ChannelAdapter, + 'StreamChannel': channels.StreamChannel, + 'NumericChannel': channels.NumericChannel, 'DatagramChannel': channels.DatagramChannel, - 'LogChannel': channels.LogChannel + 'LogChannel': channels.LogChannel }) from .services import measure globals().update({ - 'measure': measure, - 'RunBlock': measure.RunBlock, - 'StepBlock': measure.StepBlock, - 'BlockListener': measure.BlockListener, - 'ScanOperator': measure.ScanOperator, + 'measure': measure, + 'RunBlock': measure.RunBlock, + 'StepBlock': measure.StepBlock, + 'BlockListener': measure.BlockListener, + 'ScanOperator': measure.ScanOperator, 'TriggerOperator': measure.TriggerOperator, - 'LimitOperator': measure.LimitOperator, - 'StatusOperator': measure.StatusOperator, - 'Measurement': measure.Measurement + 'LimitOperator': measure.LimitOperator, + 'StatusOperator': measure.StatusOperator, + 'Measurement': measure.Measurement }) from .services import devices globals().update({ - 'loadDevice': devices.load, - 'createDevice': devices.create, - 'getDevice': devices.get, - 'getAllDevices': devices.getAll, - 'getOperator': devices.getOperator, - 'getAllOperators': devices.getAllOperators, - 'killAllDevices': devices.killAll, + 'loadDevice': devices.load, + 'createDevice': devices.create, + 'getDevice': devices.get, + 'getAllDevices': devices.getAll, + 'getOperator': devices.getOperator, + 'getAllOperators': devices.getAllOperators, + 'killAllDevices': devices.killAll, 'getAvailableDeviceName': devices.getAvailableName, - 'getDefaultDeviceName': devices.getDefaultName, - 'findDeviceForChannel': devices.findDeviceForChannel, - 'checkForDeviceLeaks': devices.checkForLeaks, - 'OperatorThread': devices.OperatorThread, - 'GenericDevice': devices.GenericDevice, - 'DisplayDevice': devices.DisplayDevice, - 'CoreDevice': devices.CoreDevice, - 'TransientDevice': devices.TransientDevice, - 'WidgetDevice': devices.WidgetDevice, - 'DeviceGroup': devices.DeviceGroup, - 'WindowGroupWidget': devices.WindowGroupWidget, - 'TabGroupWidget': devices.TabGroupWidget + 'getDefaultDeviceName': devices.getDefaultName, + 'findDeviceForChannel': devices.findDeviceForChannel, + 'checkForDeviceLeaks': devices.checkForLeaks, + 'OperatorThread': devices.OperatorThread, + 'GenericDevice': devices.GenericDevice, + 'DisplayDevice': devices.DisplayDevice, + 'CoreDevice': devices.CoreDevice, + 'TransientDevice': devices.TransientDevice, + 'WidgetDevice': devices.WidgetDevice, + 'DeviceGroup': devices.DeviceGroup, + 'WindowGroupWidget': devices.WindowGroupWidget, + 'TabGroupWidget': devices.TabGroupWidget }) from .frontend import arguments globals().update({ - 'arguments': arguments, + 'arguments': arguments, 'AbstractArgument': arguments.AbstractArgument, - 'IndexArgument': arguments.IndexArgument, + 'IndexArgument': arguments.IndexArgument, 'ComboBoxArgument': arguments.ComboBoxArgument, - 'DeviceArgument': arguments.DeviceArgument, - 'ChannelArgument': arguments.ChannelArgument, + 'DeviceArgument': arguments.DeviceArgument, + 'ChannelArgument': arguments.ChannelArgument, 'OperatorArgument': arguments.OperatorArgument, - 'FileArgument': arguments.FileArgument + 'FileArgument': arguments.FileArgument }) -def init_mp_support(): +def init_gui(): try: - core_mode - except NameError: - pass - else: - return + if globals()['load_GUI']: + return # already initialized these + except KeyError: + globals()['load_GUI'] = True + + try: + from PyQt5 import QtGui # noqa (F401) + from PyQt5 import QtWidgets # noqa (F401) + from PyQt5 import uic as QtUic + except ImportError as e: + die('An essential dependency ({0}) could not be imported and is ' + 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) + + import sys + + def die(msg): + if sys.version_info[0] == 2: + import Tkinter # different name in python2 + tkinter = Tkinter + else: + import tkinter + + root = tkinter.Tk() + root.wm_title(window_title) + + frame = tkinter.Frame(borderwidth=5) + + label = tkinter.Label(frame, justify=tkinter.LEFT, wraplength=450, + text='Fatal error during ' + 'initialization:\n\n' + msg) + label.grid(padx=5, pady=5) + + button = tkinter.Button(frame, text='Close', + command=lambda: root.quit()) + button.grid(pady=5) + + frame.grid() + frame.mainloop() + + sys.exit(0) + + globals().update({ + 'QtGui': QtGui, + 'QtWidgets': QtWidgets, + 'QtUic': QtUic, + 'die': die + }) - class _Args: - core_mode = False - kiosk_mode = False - init(_Args, 'Metro') +def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', + local_path='~/.metro', profile_path='~/.metro/profiles'): + import os + import pkg_resources + + local_path = os.path.expanduser(local_path) + os.makedirs(local_path, exist_ok=True) + + profile_path = os.path.expanduser(profile_path) + os.makedirs(profile_path, exist_ok=True) + + globals().update({ + 'WINDOW_TITLE': window_title, + 'LOCAL_PATH': local_path, + 'PROFILE_PATH': profile_path, + 'resource_exists': pkg_resources.resource_exists, + 'resource_filename': pkg_resources.resource_filename, + 'core_mode': core_mode, + 'kiosk_mode': kiosk_mode + }) + + # Initialize GUI modules if not in core mode + if not core_mode: + init_gui() + + # Initialize the core modules + init_core() def start(prog_name='metro', window_title='Metro', cli_hook=None): args, argv_left = parse_args(prog_name, cli_hook=cli_hook) - init(args, window_title) + init_metro(args.core_mode, args.kiosk_mode, window_title) from .frontend import application - if core_mode: # noqa + if core_mode: # noqa app_class = application.CoreApplication else: app_class = application.GuiApplication app = app_class(args, argv_left) app.exec_() + + +# Initialize metro per default in core mode, unless it is imported by the +# installed metro app (check for call via setuptools entry_point). +# import inspect +# if "load_entry_point('metro-sci'" not in inspect.stack()[-1][-2][0]: +# init_core() diff --git a/src/metro/devices/abstract/parallel_operator.py b/src/metro/devices/abstract/parallel_operator.py index d558d8e..7b79ef9 100755 --- a/src/metro/devices/abstract/parallel_operator.py +++ b/src/metro/devices/abstract/parallel_operator.py @@ -25,8 +25,7 @@ import traceback import metro -metro.init_mp_support() - +metro.init_metro() _targets = {} Target = collections.namedtuple('Target', ['name', 'process', 'active', From ad98c834c53a2f9be792e7913cd54c26d6a5bff7 Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Fri, 10 Jul 2020 11:55:03 +0200 Subject: [PATCH 12/20] Change global flag load_GUI --- src/metro/__init__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 05825cc..16651a8 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -54,13 +54,14 @@ def parse_args(prog_name, cli_hook=None): return cli.parse_known_args() +load_GUI = None def init_core(): - try: - if not globals()['load_GUI']: - return # already initialized in core mode - except KeyError: - globals()['load_GUI'] = False + global load_GUI + if load_GUI is None: + load_GUI = False # initialize in core mode + elif not load_GUI: + return # already initialized in core mode import sys @@ -97,7 +98,7 @@ def die(msg): 'QConsts': QtCore.Qt }) - if not globals()['load_GUI']: + if not load_GUI: class EmptyQtModule: def __getattr__(self, name): return QtCore.QObject @@ -177,11 +178,11 @@ def __getattr__(self, name): def init_gui(): - try: - if globals()['load_GUI']: - return # already initialized these - except KeyError: - globals()['load_GUI'] = True + global load_GUI + if load_GUI is None: + load_GUI = True # initialize GUI modules + elif load_GUI: + return # already initialized GUI modules try: from PyQt5 import QtGui # noqa (F401) From 602346ac148bdb3a1fb574f69c5d318a1e2e059b Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Fri, 10 Jul 2020 14:11:17 +0200 Subject: [PATCH 13/20] Fix Python version checking and 'die' routine initialization --- src/metro/__init__.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 16651a8..5aa4c10 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -65,12 +65,14 @@ def init_core(): import sys - def die(msg): - print('Fatal error during initialization: ' + msg) - sys.exit(0) + if not load_GUI: + def die(msg): + print('Fatal error during initialization: ' + msg) + sys.exit(0) + globals().update({'die': die}) if sys.version_info[:2] < (3, 3): - die('Requires python version >= 3.3 (found {0})'.format( + globals()['die']('Requires python version >= 3.3 (found {0})'.format( sys.version[:sys.version.find(' ')] )) @@ -110,7 +112,7 @@ def __getattr__(self, name): globals().update({ 'QtGui': QtGui, 'QtWidgets': QtWidgets, - 'QtUic': QtUic, + 'QtUic': QtUic }) from .services import channels @@ -184,16 +186,6 @@ def init_gui(): elif load_GUI: return # already initialized GUI modules - try: - from PyQt5 import QtGui # noqa (F401) - from PyQt5 import QtWidgets # noqa (F401) - from PyQt5 import uic as QtUic - except ImportError as e: - die('An essential dependency ({0}) could not be imported and is ' - 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) - - import sys - def die(msg): if sys.version_info[0] == 2: import Tkinter # different name in python2 @@ -202,6 +194,10 @@ def die(msg): import tkinter root = tkinter.Tk() + try: + window_title = globals()['WINDOW_TITLE'] + except KeyError: + window_title = 'Metro' root.wm_title(window_title) frame = tkinter.Frame(borderwidth=5) @@ -220,6 +216,16 @@ def die(msg): sys.exit(0) + try: + from PyQt5 import QtGui # noqa (F401) + from PyQt5 import QtWidgets # noqa (F401) + from PyQt5 import uic as QtUic + except ImportError as e: + die('An essential dependency ({0}) could not be imported and is ' + 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) + + import sys + globals().update({ 'QtGui': QtGui, 'QtWidgets': QtWidgets, From f4a3091c872ee76a6c6ce31883aac4dca8475131 Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Fri, 10 Jul 2020 14:13:14 +0200 Subject: [PATCH 14/20] Remove obsolete comments --- src/metro/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 5aa4c10..b52ed37 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -276,10 +276,3 @@ def start(prog_name='metro', window_title='Metro', cli_hook=None): app = app_class(args, argv_left) app.exec_() - - -# Initialize metro per default in core mode, unless it is imported by the -# installed metro app (check for call via setuptools entry_point). -# import inspect -# if "load_entry_point('metro-sci'" not in inspect.stack()[-1][-2][0]: -# init_core() From 1544455cbbbc5456fc1f8116d8b7ab324bfd9c6e Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Mon, 13 Jul 2020 17:24:46 +0200 Subject: [PATCH 15/20] Style adjustments for flake8 and consistence --- src/metro/__init__.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index b52ed37..528abe1 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -18,35 +18,35 @@ def parse_args(prog_name, cli_hook=None): cli_actions['profile'] = cli.add_argument( '--profile', dest='profile', action='store', type=str, - help='Load the given profile (either the name without .json ' + help='load the given profile (either the name without .json ' 'relative to the profile directory or complete path) on ' - 'startup.' + 'startup' ) mode_group = cli.add_mutually_exclusive_group() cli_actions['kiosk'] = mode_group.add_argument( '--kiosk', dest='kiosk_mode', action='store_true', - help='Start in kiosk mode with hidden controller window and a ' - 'single visible device controlling the application state.' + help='start in kiosk mode with hidden controller window and a ' + 'single visible device controlling the application state' ) cli_actions['core'] = mode_group.add_argument( '--core', dest='core_mode', action='store_true', - help='Start in core mode without a graphical interface and no ' - 'dependency on QtGui or QtWidgets.' + help='start in core mode without a graphical interface and no ' + 'dependency on QtGui or QtWidgets' ) dev_group = cli.add_argument_group(title='development flags') cli_actions['experimental'] = dev_group.add_argument( '--experimental', dest='experimental', action='store_true', - help='Turns on various experimental features.' + help='turns on various experimental features' ) cli_actions['gc-debug'] = dev_group.add_argument( '--gc-debug', dest='gc_debug', action='store', type=int, default=0, - help='Specify the debug level for the Python garbage collector.' + help='specify the debug level for the Python garbage collector' ) if cli_hook is not None: @@ -54,14 +54,16 @@ def parse_args(prog_name, cli_hook=None): return cli.parse_known_args() + load_GUI = None + def init_core(): global load_GUI if load_GUI is None: - load_GUI = False # initialize in core mode + load_GUI = False # initialize in core mode elif not load_GUI: - return # already initialized in core mode + return # already initialized in core mode import sys @@ -104,11 +106,11 @@ def die(msg): class EmptyQtModule: def __getattr__(self, name): return QtCore.QObject - + QtGui = EmptyQtModule() # noqa QtWidgets = EmptyQtModule() # noqa - QtUic = EmptyQtModule() - + QtUic = EmptyQtModule() # noqa + globals().update({ 'QtGui': QtGui, 'QtWidgets': QtWidgets, @@ -182,9 +184,9 @@ def __getattr__(self, name): def init_gui(): global load_GUI if load_GUI is None: - load_GUI = True # initialize GUI modules + load_GUI = True # initialize GUI modules elif load_GUI: - return # already initialized GUI modules + return # already initialized GUI modules def die(msg): if sys.version_info[0] == 2: @@ -255,7 +257,7 @@ def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', 'kiosk_mode': kiosk_mode }) - # Initialize GUI modules if not in core mode + # Initialize GUI modules if not in core mode if not core_mode: init_gui() From cb6a784528a90f82331a2b85dcd832ff9438083c Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Tue, 14 Jul 2020 16:22:28 +0200 Subject: [PATCH 16/20] Add source root path of the metro package to the globals --- src/metro/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 528abe1..b8f0d52 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -241,6 +241,8 @@ def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', import os import pkg_resources + src_path = os.path.dirname(os.path.realpath(__file__)) + local_path = os.path.expanduser(local_path) os.makedirs(local_path, exist_ok=True) @@ -249,6 +251,7 @@ def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', globals().update({ 'WINDOW_TITLE': window_title, + 'SRC_ROOT': src_path, 'LOCAL_PATH': local_path, 'PROFILE_PATH': profile_path, 'resource_exists': pkg_resources.resource_exists, From daa6c412b5daba7eadaa87282c08c9024307b47f Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Fri, 9 Apr 2021 11:43:59 +0200 Subject: [PATCH 17/20] Fix taskbar icon in Windows --- src/metro/__init__.py | 7 ++++--- src/metro/frontend/application.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index b8f0d52..48730a7 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -83,8 +83,9 @@ def die(msg): import numpy # noqa (F401) from PyQt5 import QtCore # noqa (F401) except ImportError as e: - die('An essential dependency ({0}) could not be imported and is ' - 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) + globals()['die']('An essential dependency ({0}) could not be ' + 'imported and is probably missing'.format( + str(e)[str(e)[:-1].rfind('\'')+1:-1])) # Populate the metro namespace with a variety of internal modules # and parts of Qt. In core mode, several of those are simulated by @@ -274,7 +275,7 @@ def start(prog_name='metro', window_title='Metro', cli_hook=None): from .frontend import application - if core_mode: # noqa + if args.core_mode: app_class = application.CoreApplication else: app_class = application.GuiApplication diff --git a/src/metro/frontend/application.py b/src/metro/frontend/application.py index 06fa711..b4785f3 100644 --- a/src/metro/frontend/application.py +++ b/src/metro/frontend/application.py @@ -57,6 +57,17 @@ def _bootstrap(self, args, version=None, version_short=None): # a PyQt5 application in this case. sys.excepthook = _on_exception + # Set AppUserModelID for Windows 7 and later so that Metro uses + # its assigned taskbar icon instead of grabbing the one with the + # same AppUserModelID (would probably result in no icon at all) + if os.name == 'nt': + try: + myappid = u"{}.{}".format(metro.SRC_ROOT, metro.WINDOW_TITLE) + from ctypes import windll + windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) + except AttributeError: + pass + metro.app = self metro.experimental = args.experimental From cce85546f9922f75ee62007db8761c92d0631ba3 Mon Sep 17 00:00:00 2001 From: Lutz Marder Date: Fri, 9 Apr 2021 16:22:06 +0200 Subject: [PATCH 18/20] Fix problem with loading invisible devices from a profile --- src/metro/services/devices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/metro/services/devices.py b/src/metro/services/devices.py index 5c18678..8db31a8 100755 --- a/src/metro/services/devices.py +++ b/src/metro/services/devices.py @@ -383,6 +383,8 @@ def _prepare(self, name, parent, args, state, entry_point): if isinstance(self, QtCore.QObject): self.destroyed.connect(_on_device_destroyed) + # for correct/full initialization set as visible first + self.setVisible(True) if 'visible' in state: self.setVisible(state['visible']) From 68b267b8d502fb3f8cff159cbe19a987d659792d Mon Sep 17 00:00:00 2001 From: ElMartes <61831317+ElMartes@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:50:19 +0200 Subject: [PATCH 19/20] Revert style changes in __init__ code, fix minor import bug, rename initialization routine back to 'init' --- src/metro/__init__.py | 143 +++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/src/metro/__init__.py b/src/metro/__init__.py index 48730a7..22d96fc 100755 --- a/src/metro/__init__.py +++ b/src/metro/__init__.py @@ -20,7 +20,7 @@ def parse_args(prog_name, cli_hook=None): '--profile', dest='profile', action='store', type=str, help='load the given profile (either the name without .json ' 'relative to the profile directory or complete path) on ' - 'startup' + 'startup.' ) mode_group = cli.add_mutually_exclusive_group() @@ -28,7 +28,7 @@ def parse_args(prog_name, cli_hook=None): cli_actions['kiosk'] = mode_group.add_argument( '--kiosk', dest='kiosk_mode', action='store_true', help='start in kiosk mode with hidden controller window and a ' - 'single visible device controlling the application state' + 'single visible device controlling the application state.' ) cli_actions['core'] = mode_group.add_argument( @@ -41,12 +41,12 @@ def parse_args(prog_name, cli_hook=None): cli_actions['experimental'] = dev_group.add_argument( '--experimental', dest='experimental', action='store_true', - help='turns on various experimental features' + help='turns on various experimental features.' ) cli_actions['gc-debug'] = dev_group.add_argument( '--gc-debug', dest='gc_debug', action='store', type=int, default=0, - help='specify the debug level for the Python garbage collector' + help='specify the debug level for the python garbage collector.' ) if cli_hook is not None: @@ -75,8 +75,7 @@ def die(msg): if sys.version_info[:2] < (3, 3): globals()['die']('Requires python version >= 3.3 (found {0})'.format( - sys.version[:sys.version.find(' ')] - )) + sys.version[:sys.version.find(' ')])) try: import typing # noqa (F401) @@ -93,14 +92,14 @@ def die(msg): # classes without any actual dependency. globals().update({ - 'QtCore': QtCore, - 'QObject': QtCore.QObject, - 'QSignal': QtCore.pyqtSignal, - 'QSlot': QtCore.pyqtSlot, + 'QtCore': QtCore, + 'QObject': QtCore.QObject, + 'QSignal': QtCore.pyqtSignal, + 'QSlot': QtCore.pyqtSlot, 'QProperty': QtCore.pyqtProperty, - 'QTimer': QtCore.QTimer, - 'QThread': QtCore.QThread, - 'QConsts': QtCore.Qt + 'QTimer': QtCore.QTimer, + 'QThread': QtCore.QThread, + 'QConsts': QtCore.Qt }) if not load_GUI: @@ -108,77 +107,77 @@ class EmptyQtModule: def __getattr__(self, name): return QtCore.QObject - QtGui = EmptyQtModule() # noqa + QtGui = EmptyQtModule() # noqa QtWidgets = EmptyQtModule() # noqa - QtUic = EmptyQtModule() # noqa + QtUic = EmptyQtModule() # noqa globals().update({ - 'QtGui': QtGui, + 'QtGui': QtGui, 'QtWidgets': QtWidgets, - 'QtUic': QtUic + 'QtUic': QtUic }) from .services import channels globals().update({ - 'channels': channels, - 'getChannel': channels.get, - 'getAllChannels': channels.getAll, - 'queryChannels': channels.query, + 'channels': channels, + 'getChannel': channels.get, + 'getAllChannels': channels.getAll, + 'queryChannels': channels.query, 'AbstractChannel': channels.AbstractChannel, - 'ChannelAdapter': channels.ChannelAdapter, - 'StreamChannel': channels.StreamChannel, - 'NumericChannel': channels.NumericChannel, + 'ChannelAdapter': channels.ChannelAdapter, + 'StreamChannel': channels.StreamChannel, + 'NumericChannel': channels.NumericChannel, 'DatagramChannel': channels.DatagramChannel, - 'LogChannel': channels.LogChannel + 'LogChannel': channels.LogChannel }) from .services import measure globals().update({ - 'measure': measure, - 'RunBlock': measure.RunBlock, - 'StepBlock': measure.StepBlock, - 'BlockListener': measure.BlockListener, - 'ScanOperator': measure.ScanOperator, + 'measure': measure, + 'RunBlock': measure.RunBlock, + 'StepBlock': measure.StepBlock, + 'BlockListener': measure.BlockListener, + 'ScanOperator': measure.ScanOperator, 'TriggerOperator': measure.TriggerOperator, - 'LimitOperator': measure.LimitOperator, - 'StatusOperator': measure.StatusOperator, - 'Measurement': measure.Measurement + 'LimitOperator': measure.LimitOperator, + 'StatusOperator': measure.StatusOperator, + 'Measurement': measure.Measurement }) from .services import devices globals().update({ - 'loadDevice': devices.load, - 'createDevice': devices.create, - 'getDevice': devices.get, - 'getAllDevices': devices.getAll, - 'getOperator': devices.getOperator, - 'getAllOperators': devices.getAllOperators, - 'killAllDevices': devices.killAll, + 'loadDevice': devices.load, + 'createDevice': devices.create, + 'getDevice': devices.get, + 'getAllDevices': devices.getAll, + 'getOperator': devices.getOperator, + 'getAllOperators': devices.getAllOperators, + 'killAllDevices': devices.killAll, 'getAvailableDeviceName': devices.getAvailableName, - 'getDefaultDeviceName': devices.getDefaultName, - 'findDeviceForChannel': devices.findDeviceForChannel, - 'checkForDeviceLeaks': devices.checkForLeaks, - 'OperatorThread': devices.OperatorThread, - 'GenericDevice': devices.GenericDevice, - 'DisplayDevice': devices.DisplayDevice, - 'CoreDevice': devices.CoreDevice, - 'TransientDevice': devices.TransientDevice, - 'WidgetDevice': devices.WidgetDevice, - 'DeviceGroup': devices.DeviceGroup, - 'WindowGroupWidget': devices.WindowGroupWidget, - 'TabGroupWidget': devices.TabGroupWidget + 'getDefaultDeviceName': devices.getDefaultName, + 'findDeviceForChannel': devices.findDeviceForChannel, + 'checkForDeviceLeaks': devices.checkForLeaks, + 'OperatorThread': devices.OperatorThread, + 'GenericDevice': devices.GenericDevice, + 'DisplayDevice': devices.DisplayDevice, + 'CoreDevice': devices.CoreDevice, + 'TransientDevice': devices.TransientDevice, + 'WidgetDevice': devices.WidgetDevice, + 'DeviceGroup': devices.DeviceGroup, + 'WindowGroupWidget': devices.WindowGroupWidget, + 'TabGroupWidget': devices.TabGroupWidget }) from .frontend import arguments globals().update({ - 'arguments': arguments, + 'arguments': arguments, 'AbstractArgument': arguments.AbstractArgument, - 'IndexArgument': arguments.IndexArgument, + 'IndexArgument': arguments.IndexArgument, 'ComboBoxArgument': arguments.ComboBoxArgument, - 'DeviceArgument': arguments.DeviceArgument, - 'ChannelArgument': arguments.ChannelArgument, + 'DeviceArgument': arguments.DeviceArgument, + 'ChannelArgument': arguments.ChannelArgument, 'OperatorArgument': arguments.OperatorArgument, - 'FileArgument': arguments.FileArgument + 'FileArgument': arguments.FileArgument }) @@ -189,6 +188,8 @@ def init_gui(): elif load_GUI: return # already initialized GUI modules + import sys + def die(msg): if sys.version_info[0] == 2: import Tkinter # different name in python2 @@ -227,18 +228,16 @@ def die(msg): die('An essential dependency ({0}) could not be imported and is ' 'probably missing'.format(str(e)[str(e)[:-1].rfind('\'')+1:-1])) - import sys - globals().update({ - 'QtGui': QtGui, + 'QtGui': QtGui, 'QtWidgets': QtWidgets, - 'QtUic': QtUic, - 'die': die + 'QtUic': QtUic, + 'die': die }) -def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', - local_path='~/.metro', profile_path='~/.metro/profiles'): +def init(core_mode=False, kiosk_mode=False, window_title='Metro', + local_path='~/.metro', profile_path='~/.metro/profiles'): import os import pkg_resources @@ -251,14 +250,14 @@ def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', os.makedirs(profile_path, exist_ok=True) globals().update({ - 'WINDOW_TITLE': window_title, - 'SRC_ROOT': src_path, - 'LOCAL_PATH': local_path, - 'PROFILE_PATH': profile_path, - 'resource_exists': pkg_resources.resource_exists, + 'WINDOW_TITLE': window_title, + 'SRC_ROOT': src_path, + 'LOCAL_PATH': local_path, + 'PROFILE_PATH': profile_path, + 'resource_exists': pkg_resources.resource_exists, 'resource_filename': pkg_resources.resource_filename, - 'core_mode': core_mode, - 'kiosk_mode': kiosk_mode + 'core_mode': core_mode, + 'kiosk_mode': kiosk_mode }) # Initialize GUI modules if not in core mode @@ -271,7 +270,7 @@ def init_metro(core_mode=False, kiosk_mode=False, window_title='Metro', def start(prog_name='metro', window_title='Metro', cli_hook=None): args, argv_left = parse_args(prog_name, cli_hook=cli_hook) - init_metro(args.core_mode, args.kiosk_mode, window_title) + init(args.core_mode, args.kiosk_mode, window_title) from .frontend import application From b4dc16cec040321ede6495edc6fbc36751ea2fb0 Mon Sep 17 00:00:00 2001 From: ElMartes <61831317+ElMartes@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:54:01 +0200 Subject: [PATCH 20/20] Fix parallel_operator to account for renaming of initialization routine --- src/metro/devices/abstract/parallel_operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/metro/devices/abstract/parallel_operator.py b/src/metro/devices/abstract/parallel_operator.py index 7b79ef9..c12b951 100755 --- a/src/metro/devices/abstract/parallel_operator.py +++ b/src/metro/devices/abstract/parallel_operator.py @@ -25,7 +25,7 @@ import traceback import metro -metro.init_metro() +metro.init() # reinitialization necessary for multiprocessing _targets = {} Target = collections.namedtuple('Target', ['name', 'process', 'active',