1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
|
import sys
import logging
import gc
import signal
from AnyQt.QtWidgets import QApplication
class WidgetPreview:
"""
A helper class for widget previews.
Attributes:
widget (OWBaseWidget): an instance of the widget or `None`
widget_cls (type): the widget class
"""
def __init__(self, widget_cls):
self.widget_cls = widget_cls
self.widget = None
logging.basicConfig()
# Allow termination with CTRL + C
signal.signal(signal.SIGINT, signal.SIG_DFL)
def run(self, input_data=None, *, no_exec=False, no_exit=False, **kwargs):
"""
Run a preview of the widget;
It first creates a widget, unless it exists from the previous call.
This can only happen if `no_exit` was set to `True`.
Next, it passes the data signals to the widget. Data given as
positional argument must be of a type for which there exist a single
or a default handler. Signals can also be given by keyword arguments,
where the name of the argument is the name of the handler method.
If the data is a list of tuples, the sequence of tuples is sent to
the same handler.
Next, the method shows the widget and starts the event loop, unless
`no_exec` argument is set to `True`.
Finally, unless the argument `no_exit` is set to `True`, the method
tears down the widget, deletes the reference to the widget and calls
Python's garbage collector, as an effort to catch any crashes due to
widget members (typically :obj:`QGraphicsScene` elements) outliving
the widget. It then calls :obj:`sys.exit` with the exit code from
the application's main loop.
If `no_exit` is set to `True`, the `run` keeps the widget alive.
In this case, subsequent calls to `run` or other methods
(`send_signals`, `exec_widget`) will use the same widget.
Args:
input_data: data used for the default input signal of matching type
no_exec (bool): if set to `True`, the widget is not shown and the
event loop is not started
no_exit (bool): if set to `True`, the widget is not torn down
**kwargs: data for input signals
"""
if self.widget is None:
self.create_widget()
self.send_signals(input_data, **kwargs)
if not no_exec:
exit_code = self.exec_widget()
else:
exit_code = 0
if not no_exit:
self.tear_down()
sys.exit(exit_code)
def create_widget(self):
"""
Initialize :obj:`QApplication` and construct the widget.
"""
global app # pylint: disable=global-variable-undefined
app = QApplication(sys.argv)
self.widget = self.widget_cls()
def send_signals(self, input_data=None, **kwargs):
"""Send signals to the widget"""
def call_handler(handler_name, data):
handler = getattr(self.widget, handler_name)
for chunk in self._data_chunks(data):
handler(*chunk)
if input_data is not None:
handler_name = self._find_handler_name(input_data)
call_handler(handler_name, input_data)
for handler_name, data in kwargs.items():
call_handler(handler_name, data)
self.widget.handleNewSignals()
def _find_handler_name(self, data):
chunk = next(self._data_chunks(data))[0]
chunk_type = type(chunk).__name__
inputs = [signal
for signal in self.widget.get_signals("inputs")
if isinstance(chunk, signal.type)]
if not inputs:
raise ValueError(f"no signal handlers for '{chunk_type}'")
if len(inputs) > 1:
inputs = [signal for signal in inputs if signal.default]
if len(inputs) != 1:
raise ValueError(
f"multiple signal handlers for '{chunk_type}'")
return inputs[0].handler
@staticmethod
def _data_chunks(data):
if isinstance(data, list) \
and data \
and all(isinstance(x, tuple) for x in data):
yield from iter(data)
elif isinstance(data, tuple):
yield data
else:
yield (data,)
def exec_widget(self):
"""Show the widget and start the :obj:`QApplication`'s main loop."""
self.widget.show()
self.widget.raise_()
return app.exec()
def tear_down(self):
"""Save settings and delete the widget."""
from AnyQt import sip
self.widget.saveSettings()
self.widget.onDeleteWidget()
sip.delete(self.widget) #: pylint: disable=c-extension-no-member
self.widget = None
gc.collect()
app.processEvents()
|