source: orange/orange/OrangeCanvas/orngSignalManager.py @ 7717:01c2e6b6c9ab

Revision 7717:01c2e6b6c9ab, 25.3 KB checked in by ales_erjavec <ales.erjavec@…>, 3 years ago (diff)
  • always process signals from the first widget after unfreeze.
Line 
1# Author: Gregor Leban (gregor.leban@fri.uni-lj.si)
2# Description:
3#    manager, that handles correct processing of widget signals
4#
5
6import sys, os, time
7import logging
8import logging.handlers
9
10import orange
11import orngDebugging
12
13Single = 2
14Multiple = 4
15
16Default = 8
17NonDefault = 16
18
19Dynamic = 32 #Dynamic output signal
20
21
22class InputSignal(object):
23    def __init__(self, name, signalType, handler, parameters = Single + NonDefault, oldParam = 0):
24        self.name = name
25        self.type = signalType
26        self.handler = handler
27
28        if type(parameters) == str: 
29            parameters = eval(parameters)   # parameters are stored as strings
30        # if we have the old definition of parameters then transform them
31        if parameters in [0,1]:
32            self.single = parameters
33            self.default = not oldParam
34            return
35
36        if not (parameters & Single or parameters & Multiple): parameters += Single
37        if not (parameters & Default or parameters & NonDefault): parameters += NonDefault
38        self.single = parameters & Single
39        self.default = parameters & Default
40       
41       
42class OutputSignal(object):
43    def __init__(self, name, signalType, parameters = Single + NonDefault):
44        self.name = name
45        self.type = signalType
46
47        if type(parameters) == str: parameters = eval(parameters)
48        if parameters in [0,1]: # old definition of parameters
49            self.default = not parameters
50            return
51
52        if not (parameters & Default or parameters & NonDefault): parameters += NonDefault
53        self.single = parameters & Single
54        self.default = parameters & Default
55        self.dynamic = parameters & Dynamic
56        if self.dynamic and self.single:
57            print "Output signal can not be Multiple and Dynamic"
58            self.dynamic = 0
59       
60               
61def canConnect(output, input, dynamic=False):
62    ret = issubclass(output.type, input.type)
63    if output.dynamic and dynamic:
64        ret = ret or issubclass(input.type,output.type)
65    return ret
66
67
68
69class SignalLink(object):
70    def __init__(self, widgetFrom, outputSignal, widgetTo, inputSignal, enabled=True):
71        self.widgetFrom = widgetFrom
72        self.widgetTo = widgetTo
73       
74        self.outputSignal = outputSignal
75        self.inputSignal = inputSignal
76       
77        if issubclass(outputSignal.type, inputSignal.type):
78            self.dynamic = False
79        else: 
80            self.dynamic = outputSignal.dynamic
81       
82        self.enabled = enabled
83   
84        self.signalNameFrom = self.outputSignal.name
85        self.signalNameTo = self.inputSignal.name
86       
87   
88    def canEnableDynamic(self, obj):
89        """ Can dynamic signal link be enabled for `obj`?
90        """
91        return isinstance(obj, self.inputSignal.type)
92   
93   
94
95# class that allows to process only one signal at a time
96class SignalWrapper(object):
97    def __init__(self, widget, method):
98        self.widget = widget
99        self.method = method
100
101    def __call__(self, *k):
102        manager = self.widget.signalManager
103        if not manager:
104            manager = signalManager
105       
106        manager.signalProcessingInProgress += 1
107        try:
108            self.method(*k)
109        finally:
110            manager.signalProcessingInProgress -= 1
111            if not manager.signalProcessingInProgress:
112                manager.processNewSignals(self.widget) 
113
114
115class SignalManager(object):
116    widgets = []    # topologically sorted list of widgets
117    links = {}      # dicionary. keys: widgetFrom, values: [SignalLink, ...]
118    freezing = 0            # do we want to process new signal immediately
119    signalProcessingInProgress = 0 # this is set to 1 when manager is propagating new signal values
120
121    def __init__(self, *args):
122        self.debugFile = None
123        self.verbosity = orngDebugging.orngVerbosity
124        self.stderr = sys.stderr
125        self._seenExceptions = {}
126        self.widgetQueue = []
127        self.asyncProcessingEnabled = False
128       
129        import orngEnviron
130        if not hasattr(self, "log"):
131            SignalManager.log = logging.getLogger("SignalManager")
132            self.logFileName = os.path.join(orngEnviron.canvasSettingsDir, "signalManager.log")
133            self.log.addHandler(logging.handlers.RotatingFileHandler(self.logFileName, maxBytes=2**20, backupCount=2))
134            self.log.setLevel(logging.INFO)
135           
136           
137        self.log.info("Signal Manager started")
138       
139        self.stdout = sys.stdout
140       
141        class err(object):
142            def write(myself, str):
143                self.log.error(str[:-1] if str.endswith("\n") else str)
144            def flush(myself):
145                pass
146        self.myerr = err()
147           
148        if orngDebugging.orngDebuggingEnabled:
149            self.debugHandler = logging.FileHandler(orngDebugging.orngDebuggingFileName, mode="wb")
150            self.log.addHandler(self.debugHandler)
151            self.log.setLevel(logging.DEBUG if orngDebugging.orngVerbosity > 0 else logging.INFO) 
152            sys.excepthook = self.exceptionHandler
153            sys.stderr = self.myerr
154
155    def setDebugMode(self, debugMode = 0, debugFileName = "signalManagerOutput.txt", verbosity = 1):
156        self.verbosity = verbosity
157
158        if debugMode:
159            handler = logging.FileHandler(debugFileName, "wb")
160            self.log.addHandler(handler)
161           
162            sys.excepthook = self.exceptionHandler
163                   
164            sys.stderr = self.myerr
165
166    # ----------------------------------------------------------
167    # ----------------------------------------------------------
168    # DEBUGGING FUNCTION
169
170    def closeDebugFile(self):
171        sys.stderr = self.stderr
172
173    def addEvent(self, strValue, object = None, eventVerbosity = 1):
174        info = str(strValue)
175        if isinstance(object, orange.ExampleTable):
176            name = " " + getattr(object, "name", "")
177            info += ". Token type = ExampleTable" + name + ". len = " + str(len(object))
178        elif type(object) == list:
179            info += ". Token type = %s. Value = %s" % (str(type(object)), str(object[:10]))
180        elif object != None:
181            info += ". Token type = %s. Value = %s" % (str(type(object)), str(object)[:100])
182        if eventVerbosity > 0:
183            self.log.debug(info)
184        else:
185            self.log.info(info)
186
187
188    def exceptionSeen(self, type, value, tracebackInfo):
189        import traceback, os
190        shortEStr = "".join(traceback.format_exception(type, value, tracebackInfo))[-2:]
191        return self._seenExceptions.has_key(shortEStr)
192
193    def exceptionHandler(self, type, value, tracebackInfo):
194        import traceback, os, StringIO
195
196        # every exception show only once
197        shortEStr = "".join(traceback.format_exception(type, value, tracebackInfo))[-2:]
198        if self._seenExceptions.has_key(shortEStr):
199            return
200        self._seenExceptions[shortEStr] = 1
201       
202        list = traceback.extract_tb(tracebackInfo, 10)
203        space = "\t"
204        totalSpace = space
205        message = StringIO.StringIO()
206        message.write("Unhandled exception of type %s\n" % ( str(type)))
207        message.write("Traceback:\n")
208
209        for i, (file, line, funct, code) in enumerate(list):
210            if not code:
211                continue
212            message.write(totalSpace + "File: " + os.path.split(file)[1] + " in line %4d\n" %(line))
213            message.write(totalSpace + "Function name: %s\n" % (funct))
214            message.write(totalSpace + "Code: " + code + "\n")
215            totalSpace += space
216
217        message.write(totalSpace[:-1] + "Exception type: " + str(type) + "\n")
218        message.write(totalSpace[:-1] + "Exception value: " + str(value)+ "\n")
219        self.log.error(message.getvalue())
220#        message.flush()
221
222    # ----------------------------------------------------------
223    # ----------------------------------------------------------
224
225    # freeze/unfreeze signal processing. If freeze=1 no signal will be processed until freeze is set back to 0
226    def setFreeze(self, freeze, startWidget = None):
227        """ Freeze/unfreeze signal processing. If freeze=1 no signal will be
228        processed until freeze is set back to 0
229       
230        """
231        self.freezing = max(freeze, 0)
232        if freeze > 0:
233            self.addEvent("Freezing signal processing (%s)" % str(freeze), startWidget)
234        elif freeze == 0:
235            self.addEvent("Unfreezing signal processing", startWidget)
236        else:
237            self.addEvent("Invalid freeze value! (by %s)", startWidget, eventVerbosity=0)
238           
239        if self.freezing == 0 and self.widgets != []:
240            self.processNewSignals(self.widgets[0]) # always start processing from the first
241#            if startWidget:
242#                self.processNewSignals(startWidget)
243#            else:
244#                self.processNewSignals(self.widgets[0])
245
246    def addWidget(self, widget):
247        """ Add `widget` to the `widgets` list
248        """
249       
250        self.addEvent("Added widget " + widget.captionTitle, eventVerbosity = 2)
251
252        if widget not in self.widgets:
253            self.widgets.append(widget)
254#            widget.connect(widget, SIGNAL("blockingStateChanged(bool)"), self.onStateChanged)
255
256    def removeWidget(self, widget):
257        """ Remove widget from the `widgets` list
258        """
259#        if self.verbosity >= 2:
260        self.addEvent("Remove widget " + widget.captionTitle, eventVerbosity = 2)
261        self.widgets.remove(widget)
262
263    def getLinks(self, widgetFrom=None, widgetTo=None, signalNameFrom=None, signalNameTo=None):
264        """ Return a list of matching SignalLinks
265        """
266        links = []
267        if widgetFrom is None:
268            widgets = self.widgets # search all widgets
269        else:
270            widgets = [widgetFrom]
271        for w in widgets:
272            for link in self.links.get(w, []):
273                if (widgetFrom is None or widgetFrom is link.widgetFrom) and \
274                   (widgetTo is None or widgetTo is link.widgetTo) and \
275                   (signalNameFrom is None or signalNameFrom == link.signalNameFrom) and \
276                   (signalNameTo is None or signalNameTo == link.signalNameTo):
277                        links.append(link)
278                   
279        return links
280
281    def getLinkWidgetsIn(self, widget, signalName):
282        """ Return a list of widgets that connect to `widget`'s input `signalName`
283        """
284        links = self.getLinks(None, widget, None, signalName)
285        return [link.widgetFrom for link in links]
286
287
288    def getLinkWidgetsOut(self, widget, signalName):
289        """ Return a list of widgets that connect to `widget`'s output `signalName`
290        """
291        links = self.getLinks(widget, None, signalName, None)
292        return [link.widgetTo for link in links]
293   
294   
295   
296    def canConnect(self, widgetFrom, widgetTo, dynamic=True):
297        # TODO: This should be retrieved from orngRegistry.WidgetDescription
298        outsignals = [OutputSignal(*tt) for tt in widgetFrom.outputs]
299        insignals = [InputSignal(*tt) for tt in widgetTo.inputs]
300       
301        return any(canConnect(out, in_, dynamic) for out in outsignals for in_ in insignals)
302       
303   
304    def proposePossibleLinks(self, widgetFrom, widgetTo, dynamic=True):
305        """ Return a ordered list of (OutputSignal, InputSignal, weight) tuples that
306        can connect both widgets
307        """
308        outSignals = [OutputSignal(*tt) for tt in widgetFrom.outputs]
309        inSignals = [InputSignal(*tt) for tt in widgetTo.inputs]
310       
311        # Get signals that are Single links and already connected to input widget
312        links = self.getLinks(None, widgetTo)
313        alreadyConnected = [link.signalNameTo for link in links if link.inputSignal.single]
314       
315        def weight(outS, inS):
316            check = [not outS.dynamic, inS.name not in alreadyConnected, bool(inS.default), bool(outS.default)] #Dynamic signals are lasts
317            weights = [2**i for i in range(len(check), 0, -1)]
318           
319            return sum([w for w, c in zip(weights, check) if c])
320       
321        possibleLinks = []
322        for outS in outSignals:
323            for inS in inSignals:
324                if canConnect(outS, inS, dynamic):
325                    possibleLinks.append((outS, inS, weight(outS, inS)))
326       
327        return sorted(possibleLinks, key=lambda link: link[-1], reverse=True)
328       
329   
330    def inputSignal(self, widget, name):
331        for tt in widget.inputs:
332            if tt[0] == name:
333                return InputSignal(*tt)
334           
335    def outputSignal(self, widget, name):
336        for tt in widget.outputs:
337            if tt[0] == name:
338                return OutputSignal(*tt)
339           
340
341    def addLink(self, widgetFrom, widgetTo, signalNameFrom, signalNameTo, enabled):
342        self.addEvent("Add link from " + widgetFrom.captionTitle + " to " + widgetTo.captionTitle, eventVerbosity = 2)
343
344        ## would this link create a cycle
345        if self.existsPath(widgetTo, widgetFrom): 
346            return 0
347        # check if signal names still exist
348        found = 0
349        for o in widgetFrom.outputs:
350            output = OutputSignal(*o)
351            if output.name == signalNameFrom: found=1
352        if not found:
353            print "Error. Widget %s changed its output signals. It does not have signal %s anymore." % (str(getattr(widgetFrom, "captionTitle", "")), signalNameFrom)
354            return 0
355
356        found = 0
357        for i in widgetTo.inputs:
358            input = InputSignal(*i)
359            if input.name == signalNameTo: found=1
360        if not found:
361            print "Error. Widget %s changed its input signals. It does not have signal %s anymore." % (str(getattr(widgetTo, "captionTitle", "")), signalNameTo)
362            return 0
363
364
365        if self.links.has_key(widgetFrom):
366            if self.getLinks(widgetFrom, widgetTo, signalNameFrom, signalNameTo):
367                print "connection ", widgetFrom, " to ", widgetTo, " alread exists. Error!!"
368                return
369
370        link = SignalLink(widgetFrom, self.outputSignal(widgetFrom, signalNameFrom),
371                          widgetTo, self.inputSignal(widgetTo, signalNameTo), enabled=enabled)
372        self.links[widgetFrom] = self.links.get(widgetFrom, []) + [link]
373
374        widgetTo.addInputConnection(widgetFrom, signalNameTo)
375
376        # if there is no key for the signalNameFrom, create it and set its id=None and data = None
377        if not widgetFrom.linksOut.has_key(signalNameFrom):
378            widgetFrom.linksOut[signalNameFrom] = {None:None}
379
380        # if channel is enabled, send data through it
381        if enabled:
382            self.pushAllOnLink(link)
383           
384        # reorder widgets if necessary
385        if self.widgets.index(widgetFrom) > self.widgets.index(widgetTo):
386            self.fixTopologicalOrdering()
387#            self.widgets.remove(widgetTo)
388#            self.widgets.append(widgetTo)   # appent the widget at the end of the list
389#            self.fixPositionOfDescendants(widgetTo)
390           
391        return 1
392
393    # fix position of descendants of widget so that the order of widgets in self.widgets is consistent with the schema
394    def fixPositionOfDescendants(self, widget):
395        for link in self.links.get(widget, []):
396            widgetTo = link.widgetTo
397            self.widgets.remove(widgetTo)
398            self.widgets.append(widgetTo)
399            self.fixPositionOfDescendants(widgetTo)
400           
401    def fixTopologicalOrdering(self):
402        """ fix the widgets topological ordering
403        """
404        order = []
405        visited = set()
406        queue = sorted([w for w in self.widgets if not self.getLinks(None, w)]) 
407        while queue:
408            w = queue.pop(0)
409            order.append(w)
410            visited.add(w)
411            linked = set([link.widgetTo for link in self.getLinks(w)])
412            queue.extend(sorted(linked.difference(queue)))
413        self.widgets[:] = order
414           
415
416    def findSignals(self, widgetFrom, widgetTo):
417        """ Return a list of (outputName, inputName) for links between widgets
418        """
419        links = self.getLinks(widgetFrom, widgetTo)
420        return [(link.signalNameFrom, link.signalNameTo) for link in links]
421   
422
423    def isSignalEnabled(self, widgetFrom, widgetTo, signalNameFrom, signalNameTo):
424        """ Is signal enabled
425        """
426        links = self.getLinks(widgetFrom, widgetTo, signalNameFrom, signalNameTo)
427        if links:
428            return links[0].enabled
429        else:
430            return False
431       
432
433    def removeLink(self, widgetFrom, widgetTo, signalNameFrom, signalNameTo):
434        """ Remove link
435        """
436        self.addEvent("Remove link from " + widgetFrom.captionTitle + " to " + widgetTo.captionTitle, eventVerbosity = 2)
437
438        # no need to update topology, just remove the link
439        if self.links.has_key(widgetFrom):
440            links = self.getLinks(widgetFrom, widgetTo, signalNameFrom, signalNameTo)
441            if len(links) != 1:
442                print "Error removing a link with none or more then one entries"
443                return
444               
445            link = links[0]
446            self.purgeLink(link)
447           
448            self.links[widgetFrom].remove(link)
449            if not self.freezing and not self.signalProcessingInProgress: 
450                self.processNewSignals(widgetFrom)
451        widgetTo.removeInputConnection(widgetFrom, signalNameTo)
452
453
454    # ############################################
455    # ENABLE OR DISABLE LINK CONNECTION
456
457    def setLinkEnabled(self, widgetFrom, widgetTo, enabled, justSend = False):
458        """ Set `enabled` state for links between widgets.
459        """
460        for link in self.getLinks(widgetFrom, widgetTo):
461            if not justSend:
462                link.enabled = enabled
463            if enabled:
464                self.pushAllOnLink(link)
465               
466        if enabled:
467            self.processNewSignals(widgetTo)
468
469
470    def getLinkEnabled(self, widgetFrom, widgetTo):
471        """ Is any link between widgets enabled
472        """
473        return any(link.enabled for link in self.getLinks(widgetFrom, widgetTo))
474
475
476    # widget widgetFrom sends signal with name signalName and value value
477    def send(self, widgetFrom, signalNameFrom, value, id):
478        """ Send signal `signalNameFrom` from `widgetFrom` with `value` and `id`
479        """
480        # add all target widgets new value and mark them as dirty
481        # if not freezed -> process dirty widgets
482        self.addEvent("Send data from " + widgetFrom.captionTitle + ". Signal = " + signalNameFrom, value, eventVerbosity = 2)
483
484        if not self.links.has_key(widgetFrom):
485            return
486       
487        for link in self.getLinks(widgetFrom, None, signalNameFrom, None):
488            self.pushToLink(link, value, id)
489
490        if not self.freezing and not self.signalProcessingInProgress:
491            self.processNewSignals(widgetFrom)
492
493    # when a new link is created, we have to
494    def sendOnNewLink(self, widgetFrom, widgetTo, signals):
495        for (signalNameFrom, signalNameTo) in signals:
496            for link in self.getLinks(widgetFrom, widgetTo, signalNameFrom, signalNameTo):
497                self.pushAllOnLink(link)
498
499
500    def pushAllOnLink(self, link):
501        """ Send all data on link
502        """
503        for key in link.widgetFrom.linksOut[link.signalNameFrom].keys():
504            self.pushToLink(link, link.widgetFrom.linksOut[link.signalNameFrom][key], key)
505
506
507    def purgeLink(self, link):
508        """ Clear all data on link (i.e. send None for all keys)
509        """
510        for key in link.widgetFrom.linksOut[link.signalNameFrom].keys():
511            self.pushToLink(link, None, key)
512           
513    def pushToLink(self, link, value, id):
514        """ Send value with id on link
515        """
516        if link.enabled:
517            if link.dynamic:
518                dyn_enable = link.canEnableDynamic(value)
519                self.setDynamicLinkEnabled(link, dyn_enable)
520                if not dyn_enable:
521                    value = None
522            link.widgetTo.updateNewSignalData(link.widgetFrom, link.signalNameTo, 
523                                          value, id, link.signalNameFrom)
524
525    def processNewSignals(self, firstWidget=None):
526        """ Process new signals starting from `firstWidget`
527        """
528       
529        if len(self.widgets) == 0 or self.signalProcessingInProgress or self.freezing:
530            return
531
532        if firstWidget not in self.widgets or self.widgetQueue:
533            firstWidget = self.widgets[0]   # if some window that is not a widget started some processing we have to process new signals from the first widget
534           
535        self.addEvent("Process new signals starting from " + firstWidget.captionTitle, eventVerbosity = 2)
536
537        skipWidgets = self.getBlockedWidgets() # Widgets that are blocking
538       
539       
540        # start propagating
541        self.signalProcessingInProgress = 1
542       
543        index = self.widgets.index(firstWidget)
544        for i in range(index, len(self.widgets)):
545            if self.widgets[i] in skipWidgets:
546                continue
547               
548            while self.widgets[i] in self.widgetQueue:
549                self.widgetQueue.remove(self.widgets[i])
550            if self.widgets[i].needProcessing:
551                self.addEvent("Processing " + self.widgets[i].captionTitle)
552                try:
553                    self.widgets[i].processSignals()
554                except Exception:
555                    type, val, traceback = sys.exc_info()
556                    sys.excepthook(type, val, traceback)  # we pretend that we handled the exception, so that it doesn't crash canvas
557                   
558                if self.widgets[i].isBlocking():
559                    if not self.asyncProcessingEnabled:
560                        self.addEvent("Widget %s blocked during signal processing. Aborting." % self.widgets[i].captionTitle)
561                        break
562                    else:
563                        self.addEvent("Widget %s blocked during signal processing." % self.widgets[i].captionTitle)           
564                   
565                    # If during signal processing the widget changed state to
566                    # blocking we skip all of its descendants
567                    skipWidgets.update(self.widgetDescendants(self.widgets[i]))
568            if self.freezing:
569                self.addEvent("Signals frozen during processing of " + self.widgets[i].captionTitle + ". Aborting.")
570                break
571       
572        # we finished propagating
573        self.signalProcessingInProgress = 0
574       
575        if self.widgetQueue:
576            # if there are still some widgets on queue
577            self.processNewSignals(None)
578       
579    def scheduleSignalProcessing(self, widget=None):
580        self.widgetQueue.append(widget)
581        self.processNewSignals(widget)
582
583
584    def existsPath(self, widgetFrom, widgetTo):
585        """ Is there a path between `widgetFrom` and `widgetTo`
586        """
587        # is there a direct link
588        if not self.links.has_key(widgetFrom):
589            return 0
590
591        for link in self.links[widgetFrom]:
592            if link.widgetTo == widgetTo:
593                return 1
594
595        # is there a nondirect link
596        for link in self.links[widgetFrom]:
597            if self.existsPath(link.widgetTo, widgetTo):
598                return 1
599
600        # there is no link...
601        return 0
602   
603
604    def widgetDescendants(self, widget):
605        """ Return all widget descendants of `widget`
606        """
607        queue = [widget]
608        queue_set = set(queue)
609       
610        index = self.widgets.index(widget)
611        for i in range(index, len(self.widgets)):
612            widget = self.widgets[i]
613            if widget not in queue:
614                continue
615            linked = [link.widgetTo for link in self.links.get(widget, []) if link.enabled]
616            for w in linked:
617                if w not in queue_set:
618                    queue.append(widget)
619                    queue_set.add(widget)
620        return queue
621   
622   
623    def isWidgetBlocked(self, widget):
624        """ Is this widget or any of its up-stream connected widgets blocked.
625        """
626        if widget.isBlocking():
627            return True
628        else:
629            widgets = [link.widgetFrom for link in self.getLinks(None, widget, None, None)]
630            if widgets:
631                return any(self.isWidgetBlocked(w) for w in widgets)
632            else:
633                return False
634           
635           
636    def getBlockedWidgets(self):
637        """ Return a set of all widgets that are blocked.
638        """
639        blocked = set()
640        for w in self.widgets:
641            if w not in blocked and w.isBlocking():
642                blocked.update(self.widgetDescendants(w))
643        return blocked
644   
645               
646    def freeze(self, widget=None):
647        """ Return a context manager that freezes the signal processing
648        """
649        signalManager = self
650        class freezer(object):
651            def __enter__(self):
652                self.push()
653                return self
654           
655            def __exit__(self, *args):
656                self.pop()
657               
658            def push(self):
659                signalManager.setFreeze(signalManager.freezing + 1)
660               
661            def pop(self):
662                signalManager.setFreeze(signalManager.freezing - 1, widget)
663               
664        return freezer()
665   
666    def setDynamicLinkEnabled(self, link, enabled):
667        import PyQt4.QtCore as QtCore
668        link.widgetFrom.emit(QtCore.SIGNAL("dynamicLinkEnabledChanged(PyQt_PyObject, bool)"), link, enabled)
669       
670   
671
672# create a global instance of signal manager
673globalSignalManager = SignalManager()
674       
675     
Note: See TracBrowser for help on using the repository browser.