Skip to content

Commit 37af799

Browse files
authored
Merge pull request #136 from janezd/auto-summary-all
Add automated summaries
2 parents a834746 + 398b520 commit 37af799

File tree

2 files changed

+180
-12
lines changed

2 files changed

+180
-12
lines changed

orangewidget/utils/signals.py

Lines changed: 179 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,73 @@
11
import copy
22
import itertools
3+
import warnings
4+
from functools import singledispatch
5+
import inspect
6+
from typing import NamedTuple, Union, Optional
7+
8+
from AnyQt.QtCore import Qt
39

410
from orangecanvas.registry.description import (
511
InputSignal, OutputSignal, Single, Multiple, Default, NonDefault,
612
Explicit, Dynamic
713
)
814

9-
from orangewidget.utils import getmembers
1015

1116
# increasing counter for ensuring the order of Input/Output definitions
1217
# is preserved when going through the unordered class namespace of
1318
# WidgetSignalsMixin.Inputs/Outputs.
1419
_counter = itertools.count()
1520

1621

22+
PartialSummary = NamedTuple(
23+
"PartialSummary", (("summary", Union[None, str, int]),
24+
("details", Optional[str])))
25+
26+
27+
def base_summarize(_) -> PartialSummary:
28+
return PartialSummary(None, None)
29+
30+
31+
summarize = singledispatch(base_summarize)
32+
33+
SUMMARY_STYLE = """
34+
<style>
35+
ul {
36+
margin-left: 4px;
37+
margin-top: 2px;
38+
-qt-list-indent:1
39+
}
40+
41+
li {
42+
margin-bottom: 3px;
43+
}
44+
45+
th {
46+
text-align: right;
47+
}
48+
</style>
49+
"""
50+
51+
52+
def can_summarize(type_, name):
53+
if not isinstance(type_, tuple):
54+
type_ = (type_, )
55+
instr = f"To silence this warning, set auto_sumarize of '{name}' to False."
56+
for a_type in type_:
57+
try:
58+
summarizer = summarize.dispatch(a_type)
59+
except TypeError:
60+
warnings.warn(f"{a_type.__name__} cannot be summarized. {instr}",
61+
UserWarning)
62+
return False
63+
if summarizer is base_summarize:
64+
warnings.warn(
65+
f"register 'summarize' function for type {a_type.__name__}. "
66+
+ instr, UserWarning)
67+
return False
68+
return True
69+
70+
1771
class _Signal:
1872
@staticmethod
1973
def get_flags(multiple, default, explicit, dynamic):
@@ -35,6 +89,15 @@ def bound_signal(self, widget):
3589
return new_signal
3690

3791

92+
def getsignals(signals_cls):
93+
# This function is preferred over getmembers because it returns the signals
94+
# in order of appearance
95+
return [(k, v)
96+
for cls in reversed(inspect.getmro(signals_cls))
97+
for k, v in cls.__dict__.items()
98+
if isinstance(v, _Signal)]
99+
100+
38101
class Input(InputSignal, _Signal):
39102
"""
40103
Description of an input signal.
@@ -76,23 +139,43 @@ def set_train_data(self, data):
76139
explicit (bool, optional):
77140
if set, this signal is only used when it is the only option or when
78141
explicitly connected in the dialog (default: `False`)
142+
auto_summary (bool, optional):
143+
if changed to `False` (default is `True`) the signal is excluded from
144+
auto summary
79145
"""
80146
def __init__(self, name, type, id=None, doc=None, replaces=None, *,
81-
multiple=False, default=False, explicit=False):
147+
multiple=False, default=False, explicit=False,
148+
auto_summary=True):
82149
flags = self.get_flags(multiple, default, explicit, False)
83150
super().__init__(name, type, "", flags, id, doc, replaces or [])
151+
self.auto_summary = auto_summary and can_summarize(type, name)
84152
self._seq_id = next(_counter)
85153

86154
def __call__(self, method):
87155
"""
88156
Decorator that stores decorated method's name in the signal's
89157
`handler` attribute. The method is returned unchanged.
90158
"""
91-
if self.handler:
159+
if self.flags & Multiple:
160+
def summarize_wrapper(widget, value, id=None):
161+
widget.set_partial_input_summary(
162+
self.name, summarize(value), id=id)
163+
method(widget, value, id)
164+
else:
165+
def summarize_wrapper(widget, value):
166+
widget.set_partial_input_summary(
167+
self.name, summarize(value))
168+
method(widget, value)
169+
170+
# Re-binding with the same name can happen in derived classes
171+
# We do not allow re-binding to a different name; for the same class
172+
# it wouldn't work, in derived class it could mislead into thinking
173+
# that the signal is passed to two different methods
174+
if self.handler and self.handler != method.__name__:
92175
raise ValueError("Input {} is already bound to method {}".
93176
format(self.name, self.handler))
94177
self.handler = method.__name__
95-
return method
178+
return summarize_wrapper if self.auto_summary else method
96179

97180

98181
class Output(OutputSignal, _Signal):
@@ -133,11 +216,16 @@ class Outputs:
133216
of the declared type and that the output can be connected to any input
134217
signal which can accept a subtype of the declared output type
135218
(default: `True`)
219+
auto_summary (bool, optional):
220+
if changed to `False` (default is `True`) the signal is excluded from
221+
auto summary
136222
"""
137223
def __init__(self, name, type, id=None, doc=None, replaces=None, *,
138-
default=False, explicit=False, dynamic=True):
224+
default=False, explicit=False, dynamic=True,
225+
auto_summary=True):
139226
flags = self.get_flags(False, default, explicit, dynamic)
140227
super().__init__(name, type, flags, id, doc, replaces or [])
228+
self.auto_summary = auto_summary and can_summarize(type, name)
141229
self.widget = None
142230
self._seq_id = next(_counter)
143231

@@ -147,6 +235,9 @@ def send(self, value, id=None):
147235
signal_manager = self.widget.signalManager
148236
if signal_manager is not None:
149237
signal_manager.send(self.widget, self.name, value, id)
238+
if self.auto_summary:
239+
self.widget.set_partial_output_summary(
240+
self.name, summarize(value), id=id)
150241

151242
def invalidate(self):
152243
"""Invalidate the current output value on the signal"""
@@ -165,14 +256,20 @@ class Outputs:
165256
pass
166257

167258
def __init__(self):
259+
self.input_summaries = {}
260+
self.output_summaries = {}
168261
self._bind_signals()
169262

170263
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)
264+
for direction, summaries in (("Inputs", self.input_summaries),
265+
("Outputs", self.output_summaries)):
266+
bound_cls = getattr(self, direction)
267+
bound_signals = bound_cls()
268+
for name, signal in getsignals(bound_cls):
269+
setattr(bound_signals, name, signal.bound_signal(self))
270+
if signal.auto_summary:
271+
summaries[signal.name] = {}
272+
setattr(self, direction, bound_signals)
176273

177274
def send(self, signalName, value, id=None):
178275
"""
@@ -222,7 +319,7 @@ def signal_from_args(args, signal_type):
222319
@classmethod
223320
def _check_input_handlers(cls):
224321
unbound = [signal.name
225-
for _, signal in getmembers(cls.Inputs, Input)
322+
for _, signal in getsignals(cls.Inputs)
226323
if not signal.handler]
227324
if unbound:
228325
raise ValueError("unbound signal(s) in {}: {}".
@@ -254,9 +351,79 @@ def get_signals(cls, direction, ignore_old_style=False):
254351
return old_style
255352

256353
signal_class = getattr(cls, direction.title())
257-
signals = [signal for _, signal in getmembers(signal_class, _Signal)]
354+
signals = [signal for _, signal in getsignals(signal_class)]
258355
return list(sorted(signals, key=lambda s: s._seq_id))
259356

357+
def update_summaries(self):
358+
self._update_summary(self.input_summaries)
359+
self._update_summary(self.output_summaries)
360+
361+
def set_partial_input_summary(self, name, partial_summary, *, id=None):
362+
self._set_part_summary(self.input_summaries[name], id, partial_summary)
363+
self._update_summary(self.input_summaries)
364+
365+
def set_partial_output_summary(self, name, partial_summary, *, id=None):
366+
self._set_part_summary(self.output_summaries[name], id, partial_summary)
367+
self._update_summary(self.output_summaries)
368+
369+
@staticmethod
370+
def _set_part_summary(summary, id, partial_summary):
371+
if partial_summary.summary is None:
372+
if id in summary:
373+
del summary[id]
374+
else:
375+
summary[id] = partial_summary
376+
377+
def _update_summary(self, summaries):
378+
from orangewidget.widget import StateInfo
379+
380+
def format_short(partial):
381+
summary = partial.summary
382+
if summary is None:
383+
return "-"
384+
if isinstance(summary, int):
385+
return StateInfo.format_number(summary)
386+
if isinstance(summary, str):
387+
return summary
388+
raise ValueError("summary must be None, string or int; "
389+
f"got {type(summary).__name__}")
390+
391+
def format_detail(partial):
392+
if partial.summary is None:
393+
return "-"
394+
return str(partial.details or partial.summary)
395+
396+
def join_multiples(partials):
397+
if not partials:
398+
return "-", "-"
399+
shorts = " ".join(map(format_short, partials.values()))
400+
details = "<br/>".join(format_detail(partial) for partial in partials.values())
401+
return shorts, details
402+
403+
info = self.info
404+
is_input = summaries is self.input_summaries
405+
assert is_input or summaries is self.output_summaries
406+
407+
if not summaries:
408+
return
409+
if not any(summaries.values()):
410+
summary = info.NoInput if is_input else info.NoOutput
411+
detail = ""
412+
else:
413+
summary, details = zip(*map(join_multiples, summaries.values()))
414+
summary = " | ".join(summary)
415+
detail = "<hr/><table>" \
416+
+ "".join(f"<tr><th><nobr>{name}</nobr>: "
417+
f"</th><td>{detail}</td></tr>"
418+
for name, detail in zip(summaries, details)) \
419+
+ "</table>"
420+
421+
setter = info.set_input_summary if is_input else info.set_output_summary
422+
if detail:
423+
setter(summary, SUMMARY_STYLE + detail, format=Qt.RichText)
424+
else:
425+
setter(summary)
426+
260427

261428
class AttributeList(list):
262429
"""Signal type for lists of attributes (variables)"""

orangewidget/widget.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ def __new__(cls, *args, captionTitle=None, **kwargs):
343343
self.__splitter = None
344344
if self.want_basic_layout:
345345
self.set_basic_layout()
346+
self.update_summaries()
346347

347348
sc = QShortcut(QKeySequence(Qt.ShiftModifier | Qt.Key_F1), self)
348349
sc.activated.connect(self.__quicktip)

0 commit comments

Comments
 (0)