11import copy
22import 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
410from 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+
1771class _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+
38101class 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
98181class 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
261428class AttributeList (list ):
262429 """Signal type for lists of attributes (variables)"""
0 commit comments