Package gavo :: Package base :: Module events
[frames] | no frames]

Source Code for Module gavo.base.events

  1  """ 
  2  General event handling. 
  3   
  4  Basically, everything roughly classified as user interaction should go 
  5  through this module.  gavo.base, on import, creates an instance of  
  6  EventDispatcher and installs it as base.ui.  The rest of the library 
  7  can then call methods of base.ui. 
  8   
  9  Clients can then register observers (probably derived from 
 10  base.observer.Observer) that subscribe to events and can display or  
 11  log them in some form appropriate to the client. 
 12  """ 
 13   
 14  #c Copyright 2008-2019, the GAVO project 
 15  #c 
 16  #c This program is free software, covered by the GNU GPL.  See the 
 17  #c COPYING file in the source distribution. 
 18   
 19   
 20  import contextlib 
 21  import sys 
 22   
 23  from gavo import utils 
 24   
 25  # the cli sets this to true if exceptions should not be mutated. 
 26  PDB_ENABLED = False 
27 28 -class DispatcherType(type):
29 """is a metaclass for dispatching of messages. 30 31 Basically, you define methods called notify<whatever> in your class. 32 For each of them, a subscribe<whatever> method is added. 33 34 Then, when notify<whatever> is called, your defined method is called, 35 and its result is then passed to all callbacks passed in through 36 subscribe<whatever>. 37 """
38 - def __init__(cls, name, bases, dict):
39 type.__init__(cls, name, bases, dict) 40 cls.eventTypes = [] 41 cls._makeNotifiers(dict)
42
43 - def _makeNotifier(cls, name, callable):
44 cls.eventTypes.append(name) 45 def notify(self, *args, **kwargs): 46 res = callable(self, *args, **kwargs) 47 for callback in self.callbacks[name]: 48 callback(res) 49 return res
50 def subscribe(self, callback): 51 self.subscribe(name, callback)
52 def unsubscribe(self, callback): 53 self.unsubscribe(name, callback) 54 setattr(cls, "notify"+name, notify) 55 setattr(cls, "subscribe"+name, subscribe) 56 setattr(cls, "unsubscribe"+name, unsubscribe) 57
58 - def _makeNotifiers(cls, dict):
59 for name, val in dict.iteritems(): 60 if name.startswith("notify"): 61 cls._makeNotifier(name[6:], val)
62
63 64 -class EventDispatcher(object):
65 """is the central event dispatcher. 66 67 Events are posted by using notify* methods. Various handlers can 68 then attach to them. 69 """ 70 __metaclass__ = DispatcherType 71
72 - def __init__(self):
73 self.callbacks = dict((name, []) for name in self.eventTypes) 74 self.sourceStack = [None] 75 self.curSource = None 76 self.totalShippedOut = 0 77 self.totalRead = 0 78 self.lastRow = None
79 80 @contextlib.contextmanager
81 - def suspended(self, evName):
82 """a context manager suspending notification for a specific event. 83 84 This is mainly for use by test code that wants to avoid spilling 85 too much junk into the log. 86 87 One weak point here is that any subscriptions entered while notification 88 is suspended are lost. So: Don't suspend notifications for normal code. 89 """ 90 origCallbacks = self.callbacks[evName] 91 self.callbacks[evName] = [] 92 try: 93 yield 94 finally: 95 self.callbacks[evName] = origCallbacks
96
97 - def subscribe(self, evName, callback):
98 self.callbacks[evName].append(callback)
99
100 - def unsubscribe(self, evName, callback):
101 """removes a callback from evName's callback list. 102 103 It is not an error to unsubscribe a callback that's not subscribed. 104 """ 105 try: 106 self.callbacks[evName].remove(callback) 107 except ValueError: 108 pass
109
110 - def notifyExceptionMutation(self, newExc):
111 """is called when an exception is being handled by raising newExc. 112 113 The callbacks are passed a pair of sys.exc_info() and newExc. 114 """ 115 return sys.exc_info(), newExc
116
117 - def logOldExc(self, newExc):
118 """notifies of and ExceptionMutation and returns newExc. 119 120 This is just a convenience when mutating exceptions. 121 """ 122 if PDB_ENABLED: 123 # leave original exception for the pdb 124 raise 125 self.notifyExceptionMutation(newExc) 126 return newExc
127
128 - def notifyNewSource(self, sourceToken):
129 """is called when a new source is being operated on. 130 131 The callbacks are passed some, hopefully useful, token string. For 132 file source, this is the file name, otherwise we try to make up 133 something. 134 135 As side effects, the curSource attribute is set to this value. 136 """ 137 sourceName = utils.makeSourceEllipsis(sourceToken) 138 self.curSource = sourceName 139 self.sourceStack.append(sourceName) 140 return sourceName
141
142 - def notifySourceError(self):
143 """is called when a parse error occurred in a source. 144 145 The callbacks are passed the name of the failing source. 146 """ 147 if self.sourceStack: # user-defined grammars may fail to push one 148 lastSource = self.sourceStack.pop() 149 else: 150 lastSource = "Undefined" 151 try: 152 self.curSource = self.sourceStack[-1] 153 except IndexError: # this would be an internal error... 154 self.curSource = None 155 return lastSource
156
157 - def notifySourceFinished(self):
158 """is called when a source file has been processed. 159 160 The curSource attribute is updated, and its old value is propagated 161 to the callbacks. 162 """ 163 try: 164 lastSource = self.sourceStack.pop() 165 self.curSource = self.sourceStack[-1] 166 except IndexError: 167 # someone didn't notified us of a finish without telling us first 168 # they started. Don't fail because of this. 169 lastSource = None 170 return lastSource
171
172 - def notifyShipout(self, numItems):
173 """is called when certain table implementations store items. 174 175 The number of items is passed on to the callbacks. As a side effect, 176 the instance variable totalShippedOut is adjusted. 177 178 InMemoryTables don't call this right now and probably never will. 179 """ 180 self.totalShippedOut += numItems 181 return numItems
182
183 - def notifyIncomingRow(self, row):
184 """is called when certain grammars yield a row to the DC's belly. 185 186 The callbacks receive a reference to the row. As a side effect, 187 the instance variable totalRead is bumped up, and lastRow becomes 188 the row passed in. 189 190 To support this, RowIterators have to call this method in their 191 _iterRows. Most will do, DictlistGrammars, e.g., don't. 192 """ 193 self.totalRead += 1 194 self.lastRow = row 195 return row
196
197 - def notifyIndexCreation(self, indexName):
198 """is called when an index on a DB table is created. 199 200 The callbacks receive the index name. 201 """ 202 return indexName
203
204 - def notifyScriptRunning(self, script):
205 """is called when a script is being started. 206 207 The callback receives a scripting.ScriptRunner instance. You probably 208 want to use the name attribute and not much else. 209 """ 210 return script
211
212 - def notifyError(self, errmsg):
213 """is called when something wants to put out an error message. 214 215 The handlers receive the error message as-is. 216 217 In general, you will be in an exception context when you receive 218 this error, but your handlers should not bomb out when you are not. 219 """ 220 return errmsg
221
222 - def notifyFailure(self, failure):
223 """is called when an unexpected twisted failure is being processed. 224 225 You should not listen on this, since the handler just receives None. 226 Rather, these events are converted to ErrorOccurreds including the 227 failure's traceback. 228 """ 229 self.notifyError(failure.getErrorMessage()+" (see info for traceback)") 230 self.notifyInfo("Traceback of failure just logged:\n%s"% 231 failure.getTraceback())
232
233 - def notifyWarning(self, message):
234 """is called when something tries to emit communicate non-fatal trouble. 235 236 The handlers receive the message as-is 237 """ 238 return message
239
240 - def notifyInfo(self, message):
241 """is called when something tries to emit auxiliary information. 242 243 The handlers receive the message as-is 244 """ 245 return message
246
247 - def notifyDebug(self, message):
248 """is called when something wants to communicate information only 249 useful when trying to figure out a malfunction. 250 251 The handlers receive the message as-is. 252 """ 253 return message
254
255 - def notifyWebServerUp(self):
256 """is called when the webserver is up and running. 257 258 No arguments are transmitted. 259 """ 260 return ()
261
262 - def notifyDBTableModified(self, fqName):
263 """is called when an existing database table has been modified. 264 265 The argument is the fully qualified table name. 266 """ 267 return fqName
268