11import copy
22import itertools
3+ import warnings
4+ from functools import singledispatch
5+ import inspect
36
47from 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+
1734class _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+
3864class 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
98140class 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
261369class AttributeList (list ):
262370 """Signal type for lists of attributes (variables)"""
0 commit comments