Skip to content

Commit 540aa85

Browse files
committed
Widget: Add automated summaries
1 parent a834746 commit 540aa85

File tree

1 file changed

+119
-11
lines changed

1 file changed

+119
-11
lines changed

orangewidget/utils/signals.py

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
import copy
22
import itertools
3+
import warnings
4+
from functools import singledispatch
5+
import inspect
36

47
from orangecanvas.registry.description import (
58
InputSignal, OutputSignal, Single, Multiple, Default, NonDefault,
69
Explicit, Dynamic
710
)
811

9-
from orangewidget.utils import getmembers
1012

1113
# increasing counter for ensuring the order of Input/Output definitions
1214
# is preserved when going through the unordered class namespace of
1315
# WidgetSignalsMixin.Inputs/Outputs.
1416
_counter = itertools.count()
1517

1618

19+
def base_summarize(_):
20+
return None, None
21+
22+
summarize = singledispatch(base_summarize)
23+
24+
def can_summarize(type_, name):
25+
if summarize.dispatch(type_) is base_summarize:
26+
warnings.warn(
27+
f"declare 'summarize' for type {type_.__name__};"
28+
f"to silence this warning, set auto_sumarize of signal '{name}' to"
29+
"False",
30+
UserWarning)
31+
return False
32+
return True
33+
1734
class _Signal:
1835
@staticmethod
1936
def get_flags(multiple, default, explicit, dynamic):
@@ -35,6 +52,15 @@ def bound_signal(self, widget):
3552
return new_signal
3653

3754

55+
def getsignals(signals_cls):
56+
# This function is preferred over getmembers because it returns the signals
57+
# in order of appearance
58+
return [(k, v)
59+
for cls in reversed(inspect.getmro(signals_cls))
60+
for k, v in cls.__dict__.items()
61+
if isinstance(v, _Signal)]
62+
63+
3864
class Input(InputSignal, _Signal):
3965
"""
4066
Description of an input signal.
@@ -76,23 +102,39 @@ def set_train_data(self, data):
76102
explicit (bool, optional):
77103
if set, this signal is only used when it is the only option or when
78104
explicitly connected in the dialog (default: `False`)
105+
auto_summary (bool, optional):
106+
if changed to `False` (default is `True`) the signal is excluded from
107+
auto summary
79108
"""
80109
def __init__(self, name, type, id=None, doc=None, replaces=None, *,
81-
multiple=False, default=False, explicit=False):
110+
multiple=False, default=False, explicit=False,
111+
auto_summary=True):
82112
flags = self.get_flags(multiple, default, explicit, False)
83113
super().__init__(name, type, "", flags, id, doc, replaces or [])
114+
self.auto_summary = auto_summary and can_summarize(type, name)
84115
self._seq_id = next(_counter)
85116

86117
def __call__(self, method):
87118
"""
88119
Decorator that stores decorated method's name in the signal's
89120
`handler` attribute. The method is returned unchanged.
90121
"""
122+
def summarize_wrapper(widget, value, *args, **kwargs):
123+
if self.auto_summary:
124+
if value is None:
125+
summary, details = widget.info.NoInput, ""
126+
else:
127+
summary, details = summarize(value)
128+
if summary is None:
129+
return
130+
widget.set_partial_input_summary(self.name, summary, details)
131+
method(widget, value, *args, **kwargs)
132+
91133
if self.handler:
92134
raise ValueError("Input {} is already bound to method {}".
93135
format(self.name, self.handler))
94136
self.handler = method.__name__
95-
return method
137+
return summarize_wrapper
96138

97139

98140
class Output(OutputSignal, _Signal):
@@ -133,17 +175,32 @@ class Outputs:
133175
of the declared type and that the output can be connected to any input
134176
signal which can accept a subtype of the declared output type
135177
(default: `True`)
178+
auto_summary (bool, optional):
179+
if changed to `False` (default is `True`) the signal is excluded from
180+
auto summary
136181
"""
137182
def __init__(self, name, type, id=None, doc=None, replaces=None, *,
138-
default=False, explicit=False, dynamic=True):
183+
default=False, explicit=False, dynamic=True,
184+
auto_summary=True):
139185
flags = self.get_flags(False, default, explicit, dynamic)
140186
super().__init__(name, type, flags, id, doc, replaces or [])
187+
self.auto_summary = auto_summary and can_summarize(type, name)
141188
self.widget = None
142189
self._seq_id = next(_counter)
143190

144191
def send(self, value, id=None):
145192
"""Emit the signal through signal manager."""
146193
assert self.widget is not None
194+
195+
if self.auto_summary:
196+
if value is None:
197+
summary, details = self.widget.info.NoOutput, ""
198+
else:
199+
summary, details = summarize(value)
200+
if summary is None:
201+
return
202+
self.widget.set_partial_output_summary(self.name, summary, details)
203+
147204
signal_manager = self.widget.signalManager
148205
if signal_manager is not None:
149206
signal_manager.send(self.widget, self.name, value, id)
@@ -165,14 +222,23 @@ class Outputs:
165222
pass
166223

167224
def __init__(self):
225+
self.input_summaries = {}
226+
self.output_summaries = {}
168227
self._bind_signals()
169228

170229
def _bind_signals(self):
171-
for direction, signal_type in (("Inputs", Input), ("Outputs", Output)):
172-
bound_cls = getattr(self, direction)()
173-
for name, signal in getmembers(bound_cls, signal_type):
174-
setattr(bound_cls, name, signal.bound_signal(self))
175-
setattr(self, direction, bound_cls)
230+
from orangewidget.widget import StateInfo
231+
232+
for direction, summaries, empty in (
233+
("Inputs", self.input_summaries, StateInfo.NoInput),
234+
("Outputs", self.output_summaries, StateInfo.NoOutput)):
235+
bound_cls = getattr(self, direction)
236+
bound_signals = bound_cls()
237+
for name, signal in getsignals(bound_cls):
238+
setattr(bound_signals, name, signal.bound_signal(self))
239+
if signal.auto_summary:
240+
summaries[signal.name] = (empty, "")
241+
setattr(self, direction, bound_signals)
176242

177243
def send(self, signalName, value, id=None):
178244
"""
@@ -222,7 +288,7 @@ def signal_from_args(args, signal_type):
222288
@classmethod
223289
def _check_input_handlers(cls):
224290
unbound = [signal.name
225-
for _, signal in getmembers(cls.Inputs, Input)
291+
for _, signal in getsignals(cls.Inputs)
226292
if not signal.handler]
227293
if unbound:
228294
raise ValueError("unbound signal(s) in {}: {}".
@@ -254,9 +320,51 @@ def get_signals(cls, direction, ignore_old_style=False):
254320
return old_style
255321

256322
signal_class = getattr(cls, direction.title())
257-
signals = [signal for _, signal in getmembers(signal_class, _Signal)]
323+
signals = [signal for _, signal in getsignals(signal_class)]
258324
return list(sorted(signals, key=lambda s: s._seq_id))
259325

326+
def set_partial_input_summary(self, name, summary, details):
327+
self.input_summaries[name] = [summary, details]
328+
self._update_summary(self.info.set_input_summary, self.input_summaries)
329+
330+
def set_partial_output_summary(self, name, summary, details):
331+
self.output_summaries[name] = (summary, details)
332+
self._update_summary(self.info.set_output_summary, self.output_summaries)
333+
334+
@staticmethod
335+
def _update_summary(setter, summaries):
336+
from orangewidget.widget import StateInfo
337+
338+
def format_short(short):
339+
if short is None or isinstance(short, StateInfo.Empty):
340+
return "-"
341+
if isinstance(short, int):
342+
return StateInfo.format_number(short)
343+
if isinstance(short, str):
344+
return short
345+
raise ValueError("summary must be None, empty, string or int; got "
346+
+ type(short).__name__)
347+
348+
def format_detail(short, detail):
349+
if short is None or isinstance(short, StateInfo.Empty):
350+
return "-"
351+
return str(detail or short)
352+
353+
if not summaries:
354+
return
355+
shorts, details = zip(*summaries.values())
356+
if len(summaries) == 1 \
357+
or all(isinstance(short, StateInfo.Empty) for short in shorts):
358+
# If all are empty, the output should be shown as empty
359+
# If there is just one output, skip the empty line and signal name
360+
summary, detail = shorts[0], details[0]
361+
else:
362+
summary = " | ".join(map(format_short, shorts))
363+
detail = "\n".join(
364+
f"\n{name}:\n{format_detail(short, detail)}"
365+
for name, short, detail in zip(summaries, shorts, details))
366+
setter(summary, detail)
367+
260368

261369
class AttributeList(list):
262370
"""Signal type for lists of attributes (variables)"""

0 commit comments

Comments
 (0)