File: widgetpreview.py

package info (click to toggle)
orange-widget-base 4.27.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,380 kB
  • sloc: python: 16,556; javascript: 58; makefile: 6
file content (134 lines) | stat: -rw-r--r-- 4,873 bytes parent folder | download | duplicates (2)
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()