source: orange/Orange/OrangeCanvas/scheme/widgetsscheme.py @ 11876:a34f5c686869

Revision 11876:a34f5c686869, 26.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 weeks ago (diff)

Fixed widget progress bar initialization.

Line 
1"""
2Widgets Scheme
3==============
4
5A Scheme for Orange Widgets Scheme (.ows).
6
7This is a subclass of the general :class:`Scheme`. It is responsible for
8the construction and management of OWBaseWidget instances corresponding
9to the scheme nodes, as well as delegating the signal propagation to a
10companion :class:`WidgetsSignalManager` class.
11
12.. autoclass:: WidgetsScheme
13   :bases:
14
15.. autoclass:: WidgetsSignalManager
16  :bases:
17
18"""
19import sys
20import logging
21
22import sip
23from PyQt4.QtGui import (
24    QShortcut, QKeySequence, QWhatsThisClickedEvent, QWidget
25)
26
27from PyQt4.QtCore import Qt, QObject, QCoreApplication, QEvent, SIGNAL
28from PyQt4.QtCore import pyqtSignal as Signal
29
30from .signalmanager import SignalManager, compress_signals, can_enable_dynamic
31from .scheme import Scheme, SchemeNode
32from .node import UserMessage
33from ..utils import name_lookup
34from ..resources import icon_loader
35
36log = logging.getLogger(__name__)
37
38
39class WidgetsScheme(Scheme):
40    """
41    A Scheme containing Orange Widgets managed with a `WidgetsSignalManager`
42    instance.
43
44    Extends the base `Scheme` class to handle the lifetime
45    (creation/deletion, etc.) of `OWBaseWidget` instances corresponding to
46    the nodes in the scheme. It also delegates the interwidget signal
47    propagation to an instance of `WidgetsSignalManager`.
48
49    """
50    def __init__(self, parent=None, title=None, description=None):
51        Scheme.__init__(self, parent, title, description)
52
53        self.signal_manager = WidgetsSignalManager(self)
54        self.widget_manager = WidgetManager()
55        self.widget_manager.set_scheme(self)
56
57    def widget_for_node(self, node):
58        """
59        Return the OWWidget instance for a `node`
60        """
61        return self.widget_manager.widget_for_node(node)
62
63    def node_for_widget(self, widget):
64        """
65        Return the SchemeNode instance for the `widget`.
66        """
67        return self.widget_manager.node_for_widget(widget)
68
69    def sync_node_properties(self):
70        """
71        Sync the widget settings/properties with the SchemeNode.properties.
72        Return True if there were any changes in the properties (i.e. if the
73        new node.properties differ from the old value) and False otherwise.
74
75        """
76        changed = False
77        for node in self.nodes:
78            widget = self.widget_for_node(node)
79            settings = widget.getSettings(alsoContexts=True)
80            if settings != node.properties:
81                node.properties = settings
82                changed = True
83        log.debug("Scheme node properties sync (changed: %s)", changed)
84        return changed
85
86    def show_report_view(self):
87        from OWReport import get_instance
88        inst = get_instance()
89        inst.show()
90        inst.raise_()
91
92
93class WidgetManager(QObject):
94    """
95    OWWidget instance manager class.
96
97    This class handles the lifetime of OWWidget instances in a
98    :class:`WidgetsScheme`.
99
100    """
101    #: A new OWWidget was created and added by the manager.
102    widget_for_node_added = Signal(SchemeNode, QWidget)
103
104    #: An OWWidget was removed, hidden and will be deleted when appropriate.
105    widget_for_node_removed = Signal(SchemeNode, QWidget)
106
107    #: Widget processing state flags:
108    #:   * InputUpdate - signal manager is updating/setting the
109    #:     widget's inputs
110    #:   * BlockingUpdate - widget has entered a blocking state
111    #:   * ProcessingUpdate - widget has entered processing state
112    InputUpdate, BlockingUpdate, ProcessingUpdate = 1, 2, 4
113
114    def __init__(self, parent=None):
115        QObject.__init__(self, parent)
116        self.__scheme = None
117        self.__signal_manager = None
118        self.__widgets = []
119        self.__widget_for_node = {}
120        self.__node_for_widget = {}
121
122        # Widgets that were 'removed' from the scheme but were at
123        # the time in an input update loop and could not be deleted
124        # immediately
125        self.__delay_delete = set()
126
127        # processing state flags for all nodes (including the ones
128        # in __delay_delete).
129        self.__widget_processing_state = {}
130
131        # Tracks the widget in the update loop by the SignalManager
132        self.__updating_widget = None
133
134    def set_scheme(self, scheme):
135        """
136        Set the :class:`WidgetsScheme` instance to manage.
137        """
138        self.__scheme = scheme
139        self.__signal_manager = scheme.findChild(SignalManager)
140
141        self.__signal_manager.processingStarted[SchemeNode].connect(
142            self.__on_processing_started
143        )
144        self.__signal_manager.processingFinished[SchemeNode].connect(
145            self.__on_processing_finished
146        )
147        scheme.node_added.connect(self.add_widget_for_node)
148        scheme.node_removed.connect(self.remove_widget_for_node)
149        scheme.installEventFilter(self)
150
151    def scheme(self):
152        """
153        Return the scheme instance on which this manager is installed.
154        """
155        return self.__scheme
156
157    def signal_manager(self):
158        """
159        Return the signal manager in use on the :func:`scheme`.
160        """
161        return self.__signal_manager
162
163    def widget_for_node(self, node):
164        """
165        Return the OWWidget instance for the scheme node.
166        """
167        return self.__widget_for_node[node]
168
169    def node_for_widget(self, widget):
170        """
171        Return the SchemeNode instance for the OWWidget.
172
173        Raise a KeyError if the widget does not map to a node in the scheme.
174        """
175        return self.__node_for_widget[widget]
176
177    def add_widget_for_node(self, node):
178        """
179        Create a new OWWidget instance for the corresponding scheme node.
180        """
181        widget = self.create_widget_instance(node)
182
183        self.__widgets.append(widget)
184        self.__widget_for_node[node] = widget
185        self.__node_for_widget[widget] = node
186
187        self.__initialize_widget_state(node, widget)
188
189        self.widget_for_node_added.emit(node, widget)
190
191    def remove_widget_for_node(self, node):
192        """
193        Remove the OWWidget instance for node.
194        """
195        widget = self.widget_for_node(node)
196
197        self.__widgets.remove(widget)
198
199        del self.__widget_for_node[node]
200        del self.__node_for_widget[widget]
201
202        node.title_changed.disconnect(widget.setCaption)
203        widget.progressBarValueChanged.disconnect(node.set_progress)
204
205        self.widget_for_node_removed.emit(node, widget)
206
207        self._delete_widget(widget)
208
209    def _delete_widget(self, widget):
210        """
211        Delete the OWBaseWidget instance.
212        """
213        widget.close()
214
215        # Save settings to user global settings.
216        if not widget._settingsFromSchema:
217            widget.saveSettings()
218
219        # Notify the widget it will be deleted.
220        widget.onDeleteWidget()
221
222        if self.__widget_processing_state[widget] != 0:
223            # If the widget is in an update loop and/or blocking we
224            # delay the scheduled deletion until the widget is done.
225            self.__delay_delete.add(widget)
226        else:
227            widget.deleteLater()
228            del self.__widget_processing_state[widget]
229
230    def create_widget_instance(self, node):
231        """
232        Create a OWWidget instance for the node.
233        """
234        desc = node.description
235        klass = name_lookup(desc.qualified_name)
236
237        log.info("Creating %r instance.", klass)
238        widget = klass.__new__(
239            klass,
240            _owInfo=True,
241            _owWarning=True,
242            _owError=True,
243            _owShowStatus=True,
244            _useContexts=True,
245            _category=desc.category,
246            _settingsFromSchema=node.properties
247        )
248
249        # Init the node/widget mapping and state before calling __init__
250        # Some OWWidgets might already send data in the constructor
251        # (should this be forbidden? Raise a warning?) triggering the signal
252        # manager which would request the widget => node mapping or state
253        self.__widget_for_node[node] = widget
254        self.__node_for_widget[widget] = node
255        self.__widget_processing_state[widget] = 0
256
257        widget.__init__(None, self.signal_manager())
258        widget.setCaption(node.title)
259        widget.widgetInfo = desc
260
261        widget.setWindowIcon(
262            icon_loader.from_description(desc).get(desc.icon)
263        )
264
265        widget.setVisible(node.properties.get("visible", False))
266
267        node.title_changed.connect(widget.setCaption)
268
269        # Widget's info/warning/error messages.
270        widget.widgetStateChanged.connect(self.__on_widget_state_changed)
271
272        # Widget's progress bar value state.
273        widget.progressBarValueChanged.connect(node.set_progress)
274
275        # Widget processing state (progressBarInit/Finished)
276        # and the blocking state.
277        widget.processingStateChanged.connect(
278            self.__on_processing_state_changed
279        )
280        self.connect(widget,
281                     SIGNAL("blockingStateChanged(bool)"),
282                     self.__on_blocking_state_changed)
283
284        if widget.isBlocking():
285            # A widget can already enter blocking state in __init__
286            self.__widget_processing_state[widget] |= self.BlockingUpdate
287
288        if widget.processingState != 0:
289            # It can also start processing (initialization of resources, ...)
290            self.__widget_processing_state[widget] |= self.ProcessingUpdate
291            node.set_processing_state(1)
292            node.set_progress(widget.progressBarValue)
293
294        # Install a help shortcut on the widget
295        help_shortcut = QShortcut(QKeySequence("F1"), widget)
296        help_shortcut.activated.connect(self.__on_help_request)
297
298        return widget
299
300    def node_processing_state(self, node):
301        """
302        Return the processing state flags for the node.
303
304        Same as `manager.node_processing_state(manger.widget_for_node(node))`
305
306        """
307        widget = self.widget_for_node(node)
308        return self.__widget_processing_state[widget]
309
310    def widget_processing_state(self, widget):
311        """
312        Return the processing state flags for the widget.
313
314        The state is an bitwise or of `InputUpdate` and `BlockingUpdate`.
315
316        """
317        return self.__widget_processing_state[widget]
318
319    def eventFilter(self, receiver, event):
320        if event.type() == QEvent.Close and receiver is self.__scheme:
321            self.signal_manager().stop()
322
323            # Notify the widget instances.
324            for widget in self.__widget_for_node.values():
325                widget.close()
326
327                if not widget._settingsFromSchema:
328                    # First save global settings if necessary.
329                    widget.saveSettings()
330
331                widget.onDeleteWidget()
332
333            event.accept()
334            return True
335
336        return QObject.eventFilter(self, receiver, event)
337
338    def __on_help_request(self):
339        """
340        Help shortcut was pressed. We send a `QWhatsThisClickedEvent` to
341        the scheme and hope someone responds to it.
342
343        """
344        # Sender is the QShortcut, and parent the OWBaseWidget
345        widget = self.sender().parent()
346        try:
347            node = self.node_for_widget(widget)
348        except KeyError:
349            pass
350        else:
351            url = "help://search?id={0}".format(node.description.id)
352            event = QWhatsThisClickedEvent(url)
353            QCoreApplication.sendEvent(self.scheme(), event)
354
355    def __initialize_widget_state(self, node, widget):
356        """
357        Initialize the tracked info/warning/error message state.
358        """
359        for message_type, state in widget.widgetState.items():
360            for message_id, message_value in state.items():
361                message = user_message_from_state(
362                    widget, message_type, message_id, message_value)
363
364                node.set_state_message(message)
365
366    def __on_widget_state_changed(self, message_type, message_id,
367                                  message_value):
368        """
369        The OWBaseWidget info/warning/error state has changed.
370
371        message_type is one of "Info", "Warning" or "Error" string depending
372        of which method (information, warning, error) was called. message_id
373        is the first int argument if supplied, and message_value the message
374        text.
375
376        """
377        widget = self.sender()
378        try:
379            node = self.node_for_widget(widget)
380        except KeyError:
381            pass
382        else:
383            message = user_message_from_state(
384                widget, str(message_type), message_id, message_value)
385
386            node.set_state_message(message)
387
388    def __on_processing_state_changed(self, state):
389        """
390        A widget processing state has changed (progressBarInit/Finished)
391        """
392        widget = self.sender()
393        try:
394            node = self.node_for_widget(widget)
395        except KeyError:
396            return
397
398        if state:
399            self.__widget_processing_state[widget] |= self.ProcessingUpdate
400        else:
401            self.__widget_processing_state[widget] &= ~self.ProcessingUpdate
402        self.__update_node_processing_state(node)
403
404    def __on_processing_started(self, node):
405        """
406        Signal manager entered the input update loop for the node.
407        """
408        widget = self.widget_for_node(node)
409        # Remember the widget instance. The node and the node->widget mapping
410        # can be removed between this and __on_processing_finished.
411        self.__updating_widget = widget
412        self.__widget_processing_state[widget] |= self.InputUpdate
413        self.__update_node_processing_state(node)
414
415    def __on_processing_finished(self, node):
416        """
417        Signal manager exited the input update loop for the node.
418        """
419        widget = self.__updating_widget
420        self.__widget_processing_state[widget] &= ~self.InputUpdate
421
422        if widget in self.__node_for_widget:
423            self.__update_node_processing_state(node)
424        elif widget in self.__delay_delete:
425            self.__try_delete(widget)
426        else:
427            raise ValueError("%r is not managed" % widget)
428
429        self.__updating_widget = None
430
431    def __on_blocking_state_changed(self, state):
432        """
433        OWWidget blocking state has changed.
434        """
435        if not state:
436            # schedule an update pass.
437            self.signal_manager()._update()
438
439        widget = self.sender()
440        if state:
441            self.__widget_processing_state[widget] |= self.BlockingUpdate
442        else:
443            self.__widget_processing_state[widget] &= ~self.BlockingUpdate
444
445        if widget in self.__node_for_widget:
446            node = self.node_for_widget(widget)
447            self.__update_node_processing_state(node)
448
449        elif widget in self.__delay_delete:
450            self.__try_delete(widget)
451
452    def __update_node_processing_state(self, node):
453        """
454        Update the `node.processing_state` to reflect the widget state.
455        """
456        state = self.node_processing_state(node)
457        node.set_processing_state(1 if state else 0)
458
459    def __try_delete(self, widget):
460        if self.__widget_processing_state[widget] == 0:
461            self.__delay_delete.remove(widget)
462            widget.deleteLater()
463            del self.__widget_processing_state[widget]
464
465
466def user_message_from_state(widget, message_type, message_id, message_value):
467    message_type = str(message_type)
468    if message_type == "Info":
469        contents = widget.widgetStateToHtml(True, False, False)
470        level = UserMessage.Info
471    elif message_type == "Warning":
472        contents = widget.widgetStateToHtml(False, True, False)
473        level = UserMessage.Warning
474    elif message_type == "Error":
475        contents = widget.widgetStateToHtml(False, False, True)
476        level = UserMessage.Error
477    else:
478        raise ValueError("Invalid message_type: %r" % message_type)
479
480    if not contents:
481        contents = None
482
483    message = UserMessage(contents, severity=level,
484                          message_id=message_type,
485                          data={"content-type": "text/html"})
486    return message
487
488
489class WidgetsSignalManager(SignalManager):
490    """
491    A signal manager for a WidgetsScheme.
492    """
493    def __init__(self, scheme):
494        SignalManager.__init__(self, scheme)
495
496        scheme.installEventFilter(self)
497
498        self.freezing = 0
499
500        self.__scheme_deleted = False
501
502        scheme.destroyed.connect(self.__on_scheme_destroyed)
503        scheme.node_added.connect(self.on_node_added)
504        scheme.node_removed.connect(self.on_node_removed)
505        scheme.link_added.connect(self.link_added)
506        scheme.link_removed.connect(self.link_removed)
507
508    def send(self, widget, channelname, value, signal_id):
509        """
510        send method compatible with OWBaseWidget.
511        """
512        scheme = self.scheme()
513        try:
514            node = scheme.node_for_widget(widget)
515        except KeyError:
516            # The Node/Widget was already removed from the scheme.
517            log.debug("Node for %r is not in the scheme.", widget)
518            return
519
520        try:
521            channel = node.output_channel(channelname)
522        except ValueError:
523            log.error("%r is not valid signal name for %r",
524                      channelname, node.description.name)
525            return
526
527        # Expand the signal_id with the unique widget id and the
528        # channel name. This is needed for OWBaseWidget's input
529        # handlers (Multiple flag).
530        signal_id = (widget.widgetId, channelname, signal_id)
531
532        SignalManager.send(self, node, channel, value, signal_id)
533
534    def is_blocking(self, node):
535        return self.scheme().widget_manager.node_processing_state(node) != 0
536
537    def send_to_node(self, node, signals):
538        """
539        Implementation of `SignalManager.send_to_node`.
540
541        Deliver input signals to an OWBaseWidget instance.
542
543        """
544        widget = self.scheme().widget_for_node(node)
545        self.process_signals_for_widget(node, widget, signals)
546
547    def compress_signals(self, signals):
548        """
549        Reimplemented from :func:`SignalManager.compress_signals`.
550        """
551        return compress_signals(signals)
552
553    def process_signals_for_widget(self, node, widget, signals):
554        """
555        Process new signals for the OWBaseWidget.
556        """
557        # This replaces the old OWBaseWidget.processSignals method
558
559        if sip.isdeleted(widget):
560            log.critical("Widget %r was deleted. Cannot process signals",
561                         widget)
562            return
563
564        app = QCoreApplication.instance()
565
566        for signal in signals:
567            link = signal.link
568            value = signal.value
569
570            # Check and update the dynamic link state
571            if link.is_dynamic():
572                link.dynamic_enabled = can_enable_dynamic(link, value)
573                if not link.dynamic_enabled:
574                    # Send None instead
575                    value = None
576
577            handler = link.sink_channel.handler
578            if handler.startswith("self."):
579                handler = handler.split(".", 1)[1]
580
581            handler = getattr(widget, handler)
582
583            if link.sink_channel.single:
584                args = (value,)
585            else:
586                args = (value, signal.id)
587
588            log.debug("Process signals: calling %s.%s (from %s with id:%s)",
589                      type(widget).__name__, handler.__name__, link, signal.id)
590
591            app.setOverrideCursor(Qt.WaitCursor)
592            try:
593                handler(*args)
594            except Exception:
595                sys.excepthook(*sys.exc_info())
596                log.exception("Error calling '%s' of '%s'",
597                              handler.__name__, node.title)
598            finally:
599                app.restoreOverrideCursor()
600
601        app.setOverrideCursor(Qt.WaitCursor)
602        try:
603            widget.handleNewSignals()
604        except Exception:
605            sys.excepthook(*sys.exc_info())
606            log.exception("Error calling 'handleNewSignals()' of '%s'",
607                          node.title)
608        finally:
609            app.restoreOverrideCursor()
610
611    def scheduleSignalProcessing(self, widget=None):
612        """
613        Back compatibility with old orngSignalManager.
614        """
615        self._update()
616
617    def processNewSignals(self, widget=None):
618        """
619        Back compatibility with old orngSignalManager.
620
621        .. todo:: The old signal manager would update immediately, but
622                  this only schedules the update. Is this a problem?
623
624        """
625        self._update()
626
627    def addEvent(self, strValue, object=None, eventVerbosity=1):
628        """
629        Back compatibility with old orngSignalManager module's logging.
630        """
631        if not isinstance(strValue, basestring):
632            info = str(strValue)
633        else:
634            info = strValue
635
636        if object is not None:
637            info += ". Token type = %s. Value = %s" % \
638                    (str(type(object)), str(object)[:100])
639
640        if eventVerbosity > 0:
641            log.debug(info)
642        else:
643            log.info(info)
644
645    def getLinks(self, widgetFrom=None, widgetTo=None,
646                 signalNameFrom=None, signalNameTo=None):
647        """
648        Back compatibility with old orngSignalManager. Some widget look if
649        they have any output connections, so this is still needed, but should
650        be deprecated in the future.
651
652        """
653        scheme = self.scheme()
654
655        source_node = sink_node = None
656
657        if widgetFrom is not None:
658            source_node = scheme.node_for_widget(widgetFrom)
659        if widgetTo is not None:
660            sink_node = scheme.node_for_widget(widgetTo)
661
662        candidates = scheme.find_links(source_node=source_node,
663                                       sink_node=sink_node)
664
665        def signallink(link):
666            """
667            Construct SignalLink from an SchemeLink.
668            """
669            w1 = scheme.widget_for_node(link.source_node)
670            w2 = scheme.widget_for_node(link.sink_node)
671
672            # Input/OutputSignal are reused from description. Interface
673            # is almost the same as it was in orngSignalManager
674            return SignalLink(w1, link.source_channel,
675                              w2, link.sink_channel,
676                              link.enabled)
677
678        links = []
679        for link in candidates:
680            if (signalNameFrom is None or \
681                    link.source_channel.name == signalNameFrom) and \
682                    (signalNameTo is None or \
683                     link.sink_channel.name == signalNameTo):
684
685                links.append(signallink(link))
686        return links
687
688    def setFreeze(self, freeze, startWidget=None):
689        """
690        Freeze/unfreeze signal processing. If freeze >= 1 no signal will be
691        processed until freeze is set back to 0.
692
693        """
694        self.freezing = max(freeze, 0)
695        if freeze > 0:
696            log.debug("Freezing signal processing (value:%r set by %r)",
697                      freeze, startWidget)
698        elif freeze == 0:
699            log.debug("Unfreezing signal processing (cleared by %r)",
700                      startWidget)
701
702        if self._input_queue:
703            self._update()
704
705    def freeze(self, widget=None):
706        """
707        Return a context manager that freezes the signal processing.
708        """
709        manager = self
710
711        class freezer(object):
712            def __enter__(self):
713                self.push()
714                return self
715
716            def __exit__(self, *args):
717                self.pop()
718
719            def push(self):
720                manager.setFreeze(manager.freezing + 1, widget)
721
722            def pop(self):
723                manager.setFreeze(manager.freezing - 1, widget)
724
725        return freezer()
726
727    def event(self, event):
728        if event.type() == QEvent.UpdateRequest:
729            if self.freezing > 0:
730                log.debug("received UpdateRequest while signal processing "
731                          "is frozen")
732                event.setAccepted(False)
733                return False
734
735            if self.__scheme_deleted:
736                log.debug("Scheme has been/is being deleted. No more "
737                          "signals will be delivered to any nodes.")
738                event.setAccepted(True)
739                return True
740        # Retain a reference to the scheme until the 'process_queued' finishes
741        # in SignalManager.event.
742        scheme = self.scheme()
743        return SignalManager.event(self, event)
744
745    def eventFilter(self, receiver, event):
746        if event.type() == QEvent.DeferredDelete and receiver is self.scheme():
747            try:
748                state = self.runtime_state()
749            except AttributeError:
750                # If the scheme (which is a parent of this object) is
751                # already being deleted the SignalManager can also be in
752                # the process of destruction (noticeable by its __dict__
753                # being empty). There is nothing really to do in this
754                # case.
755                state = None
756
757            if state == SignalManager.Processing:
758                log.info("Deferring a 'DeferredDelete' event for the Scheme "
759                         "instance until SignalManager exits the current "
760                         "update loop.")
761                event.setAccepted(False)
762                self.processingFinished.connect(self.scheme().deleteLater)
763                self.__scheme_deleted = True
764                return True
765
766        return SignalManager.eventFilter(self, receiver, event)
767
768    def __on_scheme_destroyed(self, obj):
769        self.__scheme_deleted = True
770
771
772class SignalLink(object):
773    """
774    Back compatibility with old orngSignalManager, do not use.
775    """
776    def __init__(self, widgetFrom, outputSignal, widgetTo, inputSignal,
777                 enabled=True, dynamic=False):
778        self.widgetFrom = widgetFrom
779        self.widgetTo = widgetTo
780
781        self.outputSignal = outputSignal
782        self.inputSignal = inputSignal
783
784        self.dynamic = dynamic
785
786        self.enabled = enabled
787
788        self.signalNameFrom = self.outputSignal.name
789        self.signalNameTo = self.inputSignal.name
790
791    def canEnableDynamic(self, obj):
792        """
793        Can dynamic signal link be enabled for `obj`?
794        """
795        return isinstance(obj, name_lookup(self.inputSignal.type))
796
797
798class SignalWrapper(object):
799    """
800    Signal (actually slot) wrapper used by OWBaseWidget.connect overload.
801    This disables (freezes) the widget's signal manager when slots are
802    invoked from GUI signals. Not sure if this is still needed, could instead
803    just set the blocking flag on the widget itself.
804
805    """
806    def __init__(self, widget, method):
807        self.widget = widget
808        self.method = method
809
810    def __call__(self, *args):
811        manager = self.widget.signalManager
812        if manager:
813            with manager.freeze(self.method):
814                self.method(*args)
815        else:
816            # Might be running stand alone without a manager.
817            self.method(*args)
Note: See TracBrowser for help on using the repository browser.