source: orange/orange/OrangeCanvas/orngSignalManager.py @ 7117:b587613c6e01

Revision 7117:b587613c6e01, 25.1 KB checked in by ales_erjavec <ales.erjavec@…>, 3 years ago (diff)
  • reworked the OWConcurrent API
  • added helper methods in OWBaseWidget for concurrency
  • signalManager is aware of blocking widgets, but for the moment this is useless
  • reworked the signal managers signal "freeze" functionality (signal processing can be frozen at any time in the GUI by a new Pause-Play button)
  • added Dynamic output signals (for now in use only by "Load model" and "Python Script" widget)
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:
233            self.addEvent("Freezing signal processing (%s)" % str(freeze))
234        else:
235            self.addEvent("Unfreezing signal processing")
236           
237        if not freeze and self.widgets != []:
238            if startWidget:
239                self.processNewSignals(startWidget)
240            else:
241                self.processNewSignals(self.widgets[0])
242
243    def addWidget(self, widget):
244        """ Add `widget` to the `widgets` list
245        """
246       
247        self.addEvent("Added widget " + widget.captionTitle, eventVerbosity = 2)
248
249        if widget not in self.widgets:
250            self.widgets.append(widget)
251#            widget.connect(widget, SIGNAL("blockingStateChanged(bool)"), self.onStateChanged)
252
253    def removeWidget(self, widget):
254        """ Remove widget from the `widgets` list
255        """
256#        if self.verbosity >= 2:
257        self.addEvent("Remove widget " + widget.captionTitle, eventVerbosity = 2)
258        self.widgets.remove(widget)
259
260    def getLinks(self, widgetFrom=None, widgetTo=None, signalNameFrom=None, signalNameTo=None):
261        """ Return a list of matching SignalLinks
262        """
263        links = []
264        if widgetFrom is None:
265            widgets = self.widgets # search all widgets
266        else:
267            widgets = [widgetFrom]
268        for w in widgets:
269            for link in self.links.get(w, []):
270                if (widgetFrom is None or widgetFrom is link.widgetFrom) and \
271                   (widgetTo is None or widgetTo is link.widgetTo) and \
272                   (signalNameFrom is None or signalNameFrom == link.signalNameFrom) and \
273                   (signalNameTo is None or signalNameTo == link.signalNameTo):
274                        links.append(link)
275                   
276        return links
277
278    def getLinkWidgetsIn(self, widget, signalName):
279        """ Return a list of widgets that connect to `widget`'s input `signalName`
280        """
281        links = self.getLinks(None, widget, None, signalName)
282        return [link.widgetFrom for link in links]
283
284
285    def getLinkWidgetsOut(self, widget, signalName):
286        """ Return a list of widgets that connect to `widget`'s output `signalName`
287        """
288        links = self.getLinks(widget, None, signalName, None)
289        return [link.widgetTo for link in links]
290   
291   
292   
293    def canConnect(self, widgetFrom, widgetTo, dynamic=True):
294        # TODO: This should be retrieved from orngRegistry.WidgetDescription
295        outsignals = [OutputSignal(*tt) for tt in widgetFrom.outputs]
296        insignals = [InputSignal(*tt) for tt in widgetTo.inputs]
297       
298        return any(canConnect(out, in_, dynamic) for out in outsignals for in_ in insignals)
299       
300   
301    def proposePossibleLinks(self, widgetFrom, widgetTo, dynamic=True):
302        """ Return a ordered list of (OutputSignal, InputSignal, weight) tuples that
303        can connect both widgets
304        """
305        outSignals = [OutputSignal(*tt) for tt in widgetFrom.outputs]
306        inSignals = [InputSignal(*tt) for tt in widgetTo.inputs]
307       
308        # Get signals that are Single links and already connected to input widget
309        links = self.getLinks(None, widgetTo)
310        alreadyConnected = [link.signalNameTo for link in links if link.inputSignal.single]
311       
312        def weight(outS, inS):
313            check = [not outS.dynamic, inS.name not in alreadyConnected, bool(inS.default), bool(outS.default)] #Dynamic signals are lasts
314            weights = [2**i for i in range(len(check), 0, -1)]
315           
316            return sum([w for w, c in zip(weights, check) if c])
317       
318        possibleLinks = []
319        for outS in outSignals:
320            for inS in inSignals:
321                if canConnect(outS, inS, dynamic):
322                    possibleLinks.append((outS, inS, weight(outS, inS)))
323       
324        return sorted(possibleLinks, key=lambda link: link[-1], reverse=True)
325       
326   
327    def inputSignal(self, widget, name):
328        for tt in widget.inputs:
329            if tt[0] == name:
330                return InputSignal(*tt)
331           
332    def outputSignal(self, widget, name):
333        for tt in widget.outputs:
334            if tt[0] == name:
335                return OutputSignal(*tt)
336           
337
338    def addLink(self, widgetFrom, widgetTo, signalNameFrom, signalNameTo, enabled):
339        self.addEvent("Add link from " + widgetFrom.captionTitle + " to " + widgetTo.captionTitle, eventVerbosity = 2)
340
341        ## would this link create a cycle
342        if self.existsPath(widgetTo, widgetFrom): 
343            return 0
344        # check if signal names still exist
345        found = 0
346        for o in widgetFrom.outputs:
347            output = OutputSignal(*o)
348            if output.name == signalNameFrom: found=1
349        if not found:
350            print "Error. Widget %s changed its output signals. It does not have signal %s anymore." % (str(getattr(widgetFrom, "captionTitle", "")), signalNameFrom)
351            return 0
352
353        found = 0
354        for i in widgetTo.inputs:
355            input = InputSignal(*i)
356            if input.name == signalNameTo: found=1
357        if not found:
358            print "Error. Widget %s changed its input signals. It does not have signal %s anymore." % (str(getattr(widgetTo, "captionTitle", "")), signalNameTo)
359            return 0
360
361
362        if self.links.has_key(widgetFrom):
363            if self.getLinks(widgetFrom, widgetTo, signalNameFrom, signalNameTo):
364                print "connection ", widgetFrom, " to ", widgetTo, " alread exists. Error!!"
365                return
366
367        link = SignalLink(widgetFrom, self.outputSignal(widgetFrom, signalNameFrom),
368                          widgetTo, self.inputSignal(widgetTo, signalNameTo), enabled=enabled)
369        self.links[widgetFrom] = self.links.get(widgetFrom, []) + [link]
370
371        widgetTo.addInputConnection(widgetFrom, signalNameTo)
372
373        # if there is no key for the signalNameFrom, create it and set its id=None and data = None
374        if not widgetFrom.linksOut.has_key(signalNameFrom):
375            widgetFrom.linksOut[signalNameFrom] = {None:None}
376
377        # if channel is enabled, send data through it
378        if enabled:
379            self.pushAllOnLink(link)
380           
381        # reorder widgets if necessary
382        if self.widgets.index(widgetFrom) > self.widgets.index(widgetTo):
383            self.fixTopologicalOrdering()
384#            self.widgets.remove(widgetTo)
385#            self.widgets.append(widgetTo)   # appent the widget at the end of the list
386#            self.fixPositionOfDescendants(widgetTo)
387           
388        return 1
389
390    # fix position of descendants of widget so that the order of widgets in self.widgets is consistent with the schema
391    def fixPositionOfDescendants(self, widget):
392        for link in self.links.get(widget, []):
393            widgetTo = link.widgetTo
394            self.widgets.remove(widgetTo)
395            self.widgets.append(widgetTo)
396            self.fixPositionOfDescendants(widgetTo)
397           
398    def fixTopologicalOrdering(self):
399        """ fix the widgets topological ordering
400        """
401        order = []
402        visited = set()
403        queue = sorted([w for w in self.widgets if not self.getLinks(None, w)]) 
404        while queue:
405            w = queue.pop(0)
406            order.append(w)
407            visited.add(w)
408            linked = set([link.widgetTo for link in self.getLinks(w)])
409            queue.extend(sorted(linked.difference(queue)))
410        self.widgets[:] = order
411           
412
413    def findSignals(self, widgetFrom, widgetTo):
414        """ Return a list of (outputName, inputName) for links between widgets
415        """
416        links = self.getLinks(widgetFrom, widgetTo)
417        return [(link.signalNameFrom, link.signalNameTo) for link in links]
418   
419
420    def isSignalEnabled(self, widgetFrom, widgetTo, signalNameFrom, signalNameTo):
421        """ Is signal enabled
422        """
423        links = self.getLinks(widgetFrom, widgetTo, signalNameFrom, signalNameTo)
424        if links:
425            return links[0].enabled
426        else:
427            return False
428       
429
430    def removeLink(self, widgetFrom, widgetTo, signalNameFrom, signalNameTo):
431        """ Remove link
432        """
433        self.addEvent("Remove link from " + widgetFrom.captionTitle + " to " + widgetTo.captionTitle, eventVerbosity = 2)
434
435        # no need to update topology, just remove the link
436        if self.links.has_key(widgetFrom):
437            links = self.getLinks(widgetFrom, widgetTo, signalNameFrom, signalNameTo)
438            if len(links) != 1:
439                print "Error removing a link with none or more then one entries"
440                return
441               
442            link = links[0]
443            self.purgeLink(link)
444           
445            self.links[widgetFrom].remove(link)
446            if not self.freezing and not self.signalProcessingInProgress: 
447                self.processNewSignals(widgetFrom)
448        widgetTo.removeInputConnection(widgetFrom, signalNameTo)
449
450
451    # ############################################
452    # ENABLE OR DISABLE LINK CONNECTION
453
454    def setLinkEnabled(self, widgetFrom, widgetTo, enabled, justSend = False):
455        """ Set `enabled` state for links between widgets.
456        """
457        for link in self.getLinks(widgetFrom, widgetTo):
458            if not justSend:
459                link.enabled = enabled
460            if enabled:
461                self.pushAllOnLink(link)
462               
463        if enabled:
464            self.processNewSignals(widgetTo)
465
466
467    def getLinkEnabled(self, widgetFrom, widgetTo):
468        """ Is any link between widgets enabled
469        """
470        return any(link.enabled for link in self.getLinks(widgetFrom, widgetTo))
471
472
473    # widget widgetFrom sends signal with name signalName and value value
474    def send(self, widgetFrom, signalNameFrom, value, id):
475        """ Send signal `signalNameFrom` from `widgetFrom` with `value` and `id`
476        """
477        # add all target widgets new value and mark them as dirty
478        # if not freezed -> process dirty widgets
479        self.addEvent("Send data from " + widgetFrom.captionTitle + ". Signal = " + signalNameFrom, value, eventVerbosity = 2)
480
481        if not self.links.has_key(widgetFrom):
482            return
483       
484        for link in self.getLinks(widgetFrom, None, signalNameFrom, None):
485            self.pushToLink(link, value, id)
486
487        if not self.freezing and not self.signalProcessingInProgress:
488            self.processNewSignals(widgetFrom)
489
490    # when a new link is created, we have to
491    def sendOnNewLink(self, widgetFrom, widgetTo, signals):
492        for (signalNameFrom, signalNameTo) in signals:
493            for link in self.getLinks(widgetFrom, widgetTo, signalNameFrom, signalNameTo):
494                self.pushAllOnLink(link)
495
496
497    def pushAllOnLink(self, link):
498        """ Send all data on link
499        """
500        for key in link.widgetFrom.linksOut[link.signalNameFrom].keys():
501            self.pushToLink(link, link.widgetFrom.linksOut[link.signalNameFrom][key], key)
502
503
504    def purgeLink(self, link):
505        """ Clear all data on link (i.e. send None for all keys)
506        """
507        for key in link.widgetFrom.linksOut[link.signalNameFrom].keys():
508            self.pushToLink(link, None, key)
509           
510    def pushToLink(self, link, value, id):
511        """ Send value with id on link
512        """
513        if link.enabled:
514            if link.dynamic:
515                dyn_enable = link.canEnableDynamic(value)
516                print "Dynamic signal enabled", dyn_enable
517                self.setDynamicLinkEnabled(link, dyn_enable)
518                if not dyn_enable:
519                    value = None
520            link.widgetTo.updateNewSignalData(link.widgetFrom, link.signalNameTo, 
521                                          value, id, link.signalNameFrom)
522
523    def processNewSignals(self, firstWidget=None):
524        """ Process new signals starting from `firstWidget`
525        """
526       
527        if len(self.widgets) == 0 or self.signalProcessingInProgress or self.freezing:
528            return
529
530        if firstWidget not in self.widgets or self.widgetQueue:
531            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
532           
533        self.addEvent("Process new signals starting from " + firstWidget.captionTitle, eventVerbosity = 2)
534
535        skipWidgets = self.getBlockedWidgets() # Widgets that are blocking
536       
537       
538        # start propagating
539        self.signalProcessingInProgress = 1
540       
541        index = self.widgets.index(firstWidget)
542        for i in range(index, len(self.widgets)):
543            if self.widgets[i] in skipWidgets:
544                continue
545               
546            while self.widgets[i] in self.widgetQueue:
547                self.widgetQueue.remove(self.widgets[i])
548            if self.widgets[i].needProcessing:
549                self.addEvent("Processing " + self.widgets[i].captionTitle)
550                try:
551                    self.widgets[i].processSignals()
552                except Exception:
553                    type, val, traceback = sys.exc_info()
554                    sys.excepthook(type, val, traceback)  # we pretend that we handled the exception, so that it doesn't crash canvas
555                   
556                if self.widgets[i].isBlocking():
557                    if not self.asyncProcessingEnabled:
558                        self.addEvent("Widget %s blocked during signal processing. Aborting." % self.widgets[i].captionTitle)
559                        break
560                    else:
561                        self.addEvent("Widget %s blocked during signal processing." % self.widgets[i].captionTitle)           
562                   
563                    # If during signal processing the widget changed state to
564                    # blocking we skip all of its descendants
565                    skipWidgets.update(self.widgetDescendants(self.widgets[i]))
566            if self.freezing:
567                self.addEvent("Signals frozen during processing of " + self.widgets[i].captionTitle + ". Aborting.")
568                break
569       
570        # we finished propagating
571        self.signalProcessingInProgress = 0
572       
573        if self.widgetQueue:
574            # if there are still some widgets on queue
575            self.processNewSignals(None)
576       
577    def scheduleSignalProcessing(self, widget=None):
578        self.widgetQueue.append(widget)
579        self.processNewSignals(widget)
580
581
582    def existsPath(self, widgetFrom, widgetTo):
583        """ Is there a path between `widgetFrom` and `widgetTo`
584        """
585        # is there a direct link
586        if not self.links.has_key(widgetFrom):
587            return 0
588
589        for link in self.links[widgetFrom]:
590            if link.widgetTo == widgetTo:
591                return 1
592
593        # is there a nondirect link
594        for link in self.links[widgetFrom]:
595            if self.existsPath(link.widgetTo, widgetTo):
596                return 1
597
598        # there is no link...
599        return 0
600   
601
602    def widgetDescendants(self, widget):
603        """ Return all widget descendants of `widget`
604        """
605        queue = [widget]
606        queue_set = set(queue)
607       
608        index = self.widgets.index(widget)
609        for i in range(index, len(self.widgets)):
610            widget = self.widgets[i]
611            if widget not in queue:
612                continue
613            linked = [link.widgetTo for link in self.links.get(widget, []) if link.enabled]
614            for w in linked:
615                if w not in queue_set:
616                    queue.append(widget)
617                    queue_set.add(widget)
618        return queue
619   
620   
621    def isWidgetBlocked(self, widget):
622        """ Is this widget or any of its up-stream connected widgets blocked.
623        """
624        if widget.isBlocking():
625            return True
626        else:
627            widgets = [link.widgetFrom for link in self.getLinks(None, widget, None, None)]
628            if widgets:
629                return any(self.isWidgetBlocked(w) for w in widgets)
630            else:
631                return False
632           
633           
634    def getBlockedWidgets(self):
635        """ Return a set of all widgets that are blocked.
636        """
637        blocked = set()
638        for w in self.widgets:
639            if w not in blocked and w.isBlocking():
640                blocked.update(self.widgetDescendants(w))
641        return blocked
642   
643               
644    def freeze(self, widget=None):
645        """ Return a context manager that freezes the signal processing
646        """
647        signalManager = self
648        class freezer(object):
649            def __enter__(self):
650                self.push()
651                return self
652           
653            def __exit__(self, *args):
654                self.pop()
655               
656            def push(self):
657                signalManager.setFreeze(signalManager.freezing + 1)
658               
659            def pop(self):
660                signalManager.setFreeze(signalManager.freezing - 1, widget)
661               
662        return freezer()
663   
664    def setDynamicLinkEnabled(self, link, enabled):
665        import PyQt4.QtCore as QtCore
666        link.widgetFrom.emit(QtCore.SIGNAL("dynamicLinkEnabledChanged(PyQt_PyObject, bool)"), link, enabled)
667       
668   
669
670# create a global instance of signal manager
671globalSignalManager = SignalManager()
672       
673     
Note: See TracBrowser for help on using the repository browser.