source: orange/orange/OrangeCanvas/orngSignalManager.py @ 9212:9e12e7b6ed31

Revision 9212:9e12e7b6ed31, 25.4 KB checked in by miha <miha.stajdohar@…>, 2 years ago (diff)

Do not add a log file handler if no write permissions.

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