OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.test.test_failure -*- | |
2 # See also test suite twisted.test.test_pbfailure | |
3 | |
4 # Copyright (c) 2001-2008 Twisted Matrix Laboratories. | |
5 # See LICENSE for details. | |
6 | |
7 | |
8 """ | |
9 Asynchronous-friendly error mechanism. | |
10 | |
11 See L{Failure}. | |
12 """ | |
13 | |
14 # System Imports | |
15 import sys | |
16 import linecache | |
17 import inspect | |
18 import opcode | |
19 from cStringIO import StringIO | |
20 | |
21 from twisted.python import reflect | |
22 | |
23 count = 0 | |
24 traceupLength = 4 | |
25 | |
26 class DefaultException(Exception): | |
27 pass | |
28 | |
29 def format_frames(frames, write, detail="default"): | |
30 """Format and write frames. | |
31 | |
32 @param frames: is a list of frames as used by Failure.frames, with | |
33 each frame being a list of | |
34 (funcName, fileName, lineNumber, locals.items(), globals.items()) | |
35 @type frames: list | |
36 @param write: this will be called with formatted strings. | |
37 @type write: callable | |
38 @param detail: Three detail levels are available: | |
39 default, brief, and verbose. | |
40 @type detail: string | |
41 """ | |
42 if detail not in ('default', 'brief', 'verbose'): | |
43 raise ValueError, "Detail must be default, brief, or verbose. (not %r)"
% (detail,) | |
44 w = write | |
45 if detail == "brief": | |
46 for method, filename, lineno, localVars, globalVars in frames: | |
47 w('%s:%s:%s\n' % (filename, lineno, method)) | |
48 elif detail == "default": | |
49 for method, filename, lineno, localVars, globalVars in frames: | |
50 w( ' File "%s", line %s, in %s\n' % (filename, lineno, method)) | |
51 w( ' %s\n' % linecache.getline(filename, lineno).strip()) | |
52 elif detail == "verbose": | |
53 for method, filename, lineno, localVars, globalVars in frames: | |
54 w("%s:%d: %s(...)\n" % (filename, lineno, method)) | |
55 w(' [ Locals ]\n') | |
56 # Note: the repr(val) was (self.pickled and val) or repr(val))) | |
57 for name, val in localVars: | |
58 w(" %s : %s\n" % (name, repr(val))) | |
59 w(' ( Globals )\n') | |
60 for name, val in globalVars: | |
61 w(" %s : %s\n" % (name, repr(val))) | |
62 | |
63 # slyphon: i have a need to check for this value in trial | |
64 # so I made it a module-level constant | |
65 EXCEPTION_CAUGHT_HERE = "--- <exception caught here> ---" | |
66 | |
67 | |
68 | |
69 class NoCurrentExceptionError(Exception): | |
70 """ | |
71 Raised when trying to create a Failure from the current interpreter | |
72 exception state and there is no current exception state. | |
73 """ | |
74 | |
75 | |
76 class _Traceback(object): | |
77 """ | |
78 Fake traceback object which can be passed to functions in the standard | |
79 library L{traceback} module. | |
80 """ | |
81 | |
82 def __init__(self, frames): | |
83 """ | |
84 Construct a fake traceback object using a list of frames. Note that | |
85 although frames generally include locals and globals, this information | |
86 is not kept by this object, since locals and globals are not used in | |
87 standard tracebacks. | |
88 | |
89 @param frames: [(methodname, filename, lineno, locals, globals), ...] | |
90 """ | |
91 assert len(frames) > 0, "Must pass some frames" | |
92 head, frames = frames[0], frames[1:] | |
93 name, filename, lineno, localz, globalz = head | |
94 self.tb_frame = _Frame(name, filename) | |
95 self.tb_lineno = lineno | |
96 if len(frames) == 0: | |
97 self.tb_next = None | |
98 else: | |
99 self.tb_next = _Traceback(frames) | |
100 | |
101 | |
102 class _Frame(object): | |
103 """ | |
104 A fake frame object, used by L{_Traceback}. | |
105 """ | |
106 | |
107 def __init__(self, name, filename): | |
108 self.f_code = _Code(name, filename) | |
109 self.f_globals = {} | |
110 | |
111 | |
112 class _Code(object): | |
113 """ | |
114 A fake code object, used by L{_Traceback} via L{_Frame}. | |
115 """ | |
116 def __init__(self, name, filename): | |
117 self.co_name = name | |
118 self.co_filename = filename | |
119 | |
120 | |
121 class Failure: | |
122 """A basic abstraction for an error that has occurred. | |
123 | |
124 This is necessary because Python's built-in error mechanisms are | |
125 inconvenient for asynchronous communication. | |
126 | |
127 @ivar value: The exception instance responsible for this failure. | |
128 @ivar type: The exception's class. | |
129 """ | |
130 | |
131 pickled = 0 | |
132 stack = None | |
133 | |
134 # The opcode of "yield" in Python bytecode. We need this in _findFailure in | |
135 # order to identify whether an exception was thrown by a | |
136 # throwExceptionIntoGenerator. | |
137 _yieldOpcode = chr(opcode.opmap["YIELD_VALUE"]) | |
138 | |
139 def __init__(self, exc_value=None, exc_type=None, exc_tb=None): | |
140 """Initialize me with an explanation of the error. | |
141 | |
142 By default, this will use the current X{exception} | |
143 (L{sys.exc_info}()). However, if you want to specify a | |
144 particular kind of failure, you can pass an exception as an | |
145 argument. | |
146 | |
147 If no C{exc_value} is passed, then an "original" Failure will | |
148 be searched for. If the current exception handler that this | |
149 Failure is being constructed in is handling an exception | |
150 raised by L{raiseException}, then this Failure will act like | |
151 the original Failure. | |
152 """ | |
153 global count | |
154 count = count + 1 | |
155 self.count = count | |
156 self.type = self.value = tb = None | |
157 | |
158 #strings Exceptions/Failures are bad, mmkay? | |
159 if isinstance(exc_value, (str, unicode)) and exc_type is None: | |
160 import warnings | |
161 warnings.warn( | |
162 "Don't pass strings (like %r) to failure.Failure (replacing with
a DefaultException)." % | |
163 exc_value, DeprecationWarning, stacklevel=2) | |
164 exc_value = DefaultException(exc_value) | |
165 | |
166 stackOffset = 0 | |
167 | |
168 if exc_value is None: | |
169 exc_value = self._findFailure() | |
170 | |
171 if exc_value is None: | |
172 self.type, self.value, tb = sys.exc_info() | |
173 if self.type is None: | |
174 raise NoCurrentExceptionError() | |
175 stackOffset = 1 | |
176 elif exc_type is None: | |
177 if isinstance(exc_value, Exception): | |
178 self.type = exc_value.__class__ | |
179 else: #allow arbitrary objects. | |
180 self.type = type(exc_value) | |
181 self.value = exc_value | |
182 else: | |
183 self.type = exc_type | |
184 self.value = exc_value | |
185 if isinstance(self.value, Failure): | |
186 self.__dict__ = self.value.__dict__ | |
187 return | |
188 if tb is None: | |
189 if exc_tb: | |
190 tb = exc_tb | |
191 # else: | |
192 # log.msg("Erf, %r created with no traceback, %s %s." % ( | |
193 # repr(self), repr(exc_value), repr(exc_type))) | |
194 # for s in traceback.format_stack(): | |
195 # log.msg(s) | |
196 | |
197 frames = self.frames = [] | |
198 stack = self.stack = [] | |
199 | |
200 # added 2003-06-23 by Chris Armstrong. Yes, I actually have a | |
201 # use case where I need this traceback object, and I've made | |
202 # sure that it'll be cleaned up. | |
203 self.tb = tb | |
204 | |
205 if tb: | |
206 f = tb.tb_frame | |
207 elif not isinstance(self.value, Failure): | |
208 # we don't do frame introspection since it's expensive, | |
209 # and if we were passed a plain exception with no | |
210 # traceback, it's not useful anyway | |
211 f = stackOffset = None | |
212 | |
213 while stackOffset and f: | |
214 # This excludes this Failure.__init__ frame from the | |
215 # stack, leaving it to start with our caller instead. | |
216 f = f.f_back | |
217 stackOffset -= 1 | |
218 | |
219 # Keeps the *full* stack. Formerly in spread.pb.print_excFullStack: | |
220 # | |
221 # The need for this function arises from the fact that several | |
222 # PB classes have the peculiar habit of discarding exceptions | |
223 # with bareword "except:"s. This premature exception | |
224 # catching means tracebacks generated here don't tend to show | |
225 # what called upon the PB object. | |
226 | |
227 while f: | |
228 localz = f.f_locals.copy() | |
229 if f.f_locals is f.f_globals: | |
230 globalz = {} | |
231 else: | |
232 globalz = f.f_globals.copy() | |
233 for d in globalz, localz: | |
234 if d.has_key("__builtins__"): | |
235 del d["__builtins__"] | |
236 stack.insert(0, [ | |
237 f.f_code.co_name, | |
238 f.f_code.co_filename, | |
239 f.f_lineno, | |
240 localz.items(), | |
241 globalz.items(), | |
242 ]) | |
243 f = f.f_back | |
244 | |
245 while tb is not None: | |
246 f = tb.tb_frame | |
247 localz = f.f_locals.copy() | |
248 if f.f_locals is f.f_globals: | |
249 globalz = {} | |
250 else: | |
251 globalz = f.f_globals.copy() | |
252 for d in globalz, localz: | |
253 if d.has_key("__builtins__"): | |
254 del d["__builtins__"] | |
255 | |
256 frames.append([ | |
257 f.f_code.co_name, | |
258 f.f_code.co_filename, | |
259 tb.tb_lineno, | |
260 localz.items(), | |
261 globalz.items(), | |
262 ]) | |
263 tb = tb.tb_next | |
264 if inspect.isclass(self.type) and issubclass(self.type, Exception): | |
265 parentCs = reflect.allYourBase(self.type) | |
266 self.parents = map(reflect.qual, parentCs) | |
267 self.parents.append(reflect.qual(self.type)) | |
268 else: | |
269 self.parents = [self.type] | |
270 | |
271 def trap(self, *errorTypes): | |
272 """Trap this failure if its type is in a predetermined list. | |
273 | |
274 This allows you to trap a Failure in an error callback. It will be | |
275 automatically re-raised if it is not a type that you expect. | |
276 | |
277 The reason for having this particular API is because it's very useful | |
278 in Deferred errback chains: | |
279 | |
280 | def _ebFoo(self, failure): | |
281 | r = failure.trap(Spam, Eggs) | |
282 | print 'The Failure is due to either Spam or Eggs!' | |
283 | if r == Spam: | |
284 | print 'Spam did it!' | |
285 | elif r == Eggs: | |
286 | print 'Eggs did it!' | |
287 | |
288 If the failure is not a Spam or an Eggs, then the Failure | |
289 will be 'passed on' to the next errback. | |
290 | |
291 @type errorTypes: L{Exception} | |
292 """ | |
293 error = self.check(*errorTypes) | |
294 if not error: | |
295 raise self | |
296 return error | |
297 | |
298 def check(self, *errorTypes): | |
299 """Check if this failure's type is in a predetermined list. | |
300 | |
301 @type errorTypes: list of L{Exception} classes or | |
302 fully-qualified class names. | |
303 @returns: the matching L{Exception} type, or None if no match. | |
304 """ | |
305 for error in errorTypes: | |
306 err = error | |
307 if inspect.isclass(error) and issubclass(error, Exception): | |
308 err = reflect.qual(error) | |
309 if err in self.parents: | |
310 return error | |
311 return None | |
312 | |
313 | |
314 def raiseException(self): | |
315 """ | |
316 raise the original exception, preserving traceback | |
317 information if available. | |
318 """ | |
319 raise self.type, self.value, self.tb | |
320 | |
321 | |
322 def throwExceptionIntoGenerator(self, g): | |
323 """ | |
324 Throw the original exception into the given generator, | |
325 preserving traceback information if available. | |
326 | |
327 @return: The next value yielded from the generator. | |
328 @raise StopIteration: If there are no more values in the generator. | |
329 @raise anything else: Anything that the generator raises. | |
330 """ | |
331 return g.throw(self.type, self.value, self.tb) | |
332 | |
333 | |
334 def _findFailure(cls): | |
335 """ | |
336 Find the failure that represents the exception currently in context. | |
337 """ | |
338 tb = sys.exc_info()[-1] | |
339 if not tb: | |
340 return | |
341 | |
342 secondLastTb = None | |
343 lastTb = tb | |
344 while lastTb.tb_next: | |
345 secondLastTb = lastTb | |
346 lastTb = lastTb.tb_next | |
347 | |
348 lastFrame = lastTb.tb_frame | |
349 | |
350 # NOTE: f_locals.get('self') is used rather than | |
351 # f_locals['self'] because psyco frames do not contain | |
352 # anything in their locals() dicts. psyco makes debugging | |
353 # difficult anyhow, so losing the Failure objects (and thus | |
354 # the tracebacks) here when it is used is not that big a deal. | |
355 | |
356 # handle raiseException-originated exceptions | |
357 if lastFrame.f_code is cls.raiseException.func_code: | |
358 return lastFrame.f_locals.get('self') | |
359 | |
360 # handle throwExceptionIntoGenerator-originated exceptions | |
361 # this is tricky, and differs if the exception was caught | |
362 # inside the generator, or above it: | |
363 | |
364 # it is only really originating from | |
365 # throwExceptionIntoGenerator if the bottom of the traceback | |
366 # is a yield. | |
367 # Pyrex and Cython extensions create traceback frames | |
368 # with no co_code, but they can't yield so we know it's okay to just ret
urn here. | |
369 if ((not lastFrame.f_code.co_code) or | |
370 lastFrame.f_code.co_code[lastTb.tb_lasti] != cls._yieldOpcode): | |
371 return | |
372 | |
373 # if the exception was caught above the generator.throw | |
374 # (outside the generator), it will appear in the tb (as the | |
375 # second last item): | |
376 if secondLastTb: | |
377 frame = secondLastTb.tb_frame | |
378 if frame.f_code is cls.throwExceptionIntoGenerator.func_code: | |
379 return frame.f_locals.get('self') | |
380 | |
381 # if the exception was caught below the generator.throw | |
382 # (inside the generator), it will appear in the frames' linked | |
383 # list, above the top-level traceback item (which must be the | |
384 # generator frame itself, thus its caller is | |
385 # throwExceptionIntoGenerator). | |
386 frame = tb.tb_frame.f_back | |
387 if frame and frame.f_code is cls.throwExceptionIntoGenerator.func_code: | |
388 return frame.f_locals.get('self') | |
389 | |
390 _findFailure = classmethod(_findFailure) | |
391 | |
392 def __repr__(self): | |
393 return "<%s %s>" % (self.__class__, self.type) | |
394 | |
395 def __str__(self): | |
396 return "[Failure instance: %s]" % self.getBriefTraceback() | |
397 | |
398 def __getstate__(self): | |
399 """Avoid pickling objects in the traceback. | |
400 """ | |
401 if self.pickled: | |
402 return self.__dict__ | |
403 c = self.__dict__.copy() | |
404 | |
405 c['frames'] = [ | |
406 [ | |
407 v[0], v[1], v[2], | |
408 [(j[0], reflect.safe_repr(j[1])) for j in v[3]], | |
409 [(j[0], reflect.safe_repr(j[1])) for j in v[4]] | |
410 ] for v in self.frames | |
411 ] | |
412 | |
413 # added 2003-06-23. See comment above in __init__ | |
414 c['tb'] = None | |
415 | |
416 if self.stack is not None: | |
417 # XXX: This is a band-aid. I can't figure out where these | |
418 # (failure.stack is None) instances are coming from. | |
419 c['stack'] = [ | |
420 [ | |
421 v[0], v[1], v[2], | |
422 [(j[0], reflect.safe_repr(j[1])) for j in v[3]], | |
423 [(j[0], reflect.safe_repr(j[1])) for j in v[4]] | |
424 ] for v in self.stack | |
425 ] | |
426 | |
427 c['pickled'] = 1 | |
428 return c | |
429 | |
430 def cleanFailure(self): | |
431 """Remove references to other objects, replacing them with strings. | |
432 """ | |
433 self.__dict__ = self.__getstate__() | |
434 | |
435 def getTracebackObject(self): | |
436 """ | |
437 Get an object that represents this Failure's stack that can be passed | |
438 to traceback.extract_tb. | |
439 | |
440 If the original traceback object is still present, return that. If this | |
441 traceback object has been lost but we still have the information, | |
442 return a fake traceback object (see L{_Traceback}). If there is no | |
443 traceback information at all, return None. | |
444 """ | |
445 if self.tb is not None: | |
446 return self.tb | |
447 elif len(self.frames) > 0: | |
448 return _Traceback(self.frames) | |
449 else: | |
450 return None | |
451 | |
452 def getErrorMessage(self): | |
453 """Get a string of the exception which caused this Failure.""" | |
454 if isinstance(self.value, Failure): | |
455 return self.value.getErrorMessage() | |
456 return reflect.safe_str(self.value) | |
457 | |
458 def getBriefTraceback(self): | |
459 io = StringIO() | |
460 self.printBriefTraceback(file=io) | |
461 return io.getvalue() | |
462 | |
463 def getTraceback(self, elideFrameworkCode=0, detail='default'): | |
464 io = StringIO() | |
465 self.printTraceback(file=io, elideFrameworkCode=elideFrameworkCode, deta
il=detail) | |
466 return io.getvalue() | |
467 | |
468 def printTraceback(self, file=None, elideFrameworkCode=0, detail='default'): | |
469 """Emulate Python's standard error reporting mechanism. | |
470 """ | |
471 if file is None: | |
472 file = log.logerr | |
473 w = file.write | |
474 | |
475 # Preamble | |
476 if detail == 'verbose': | |
477 w( '*--- Failure #%d%s---\n' % | |
478 (self.count, | |
479 (self.pickled and ' (pickled) ') or ' ')) | |
480 elif detail == 'brief': | |
481 if self.frames: | |
482 hasFrames = 'Traceback' | |
483 else: | |
484 hasFrames = 'Traceback (failure with no frames)' | |
485 w("%s: %s: %s\n" % (hasFrames, self.type, self.value)) | |
486 else: | |
487 w( 'Traceback (most recent call last):\n') | |
488 | |
489 # Frames, formatted in appropriate style | |
490 if self.frames: | |
491 if not elideFrameworkCode: | |
492 format_frames(self.stack[-traceupLength:], w, detail) | |
493 w("%s\n" % (EXCEPTION_CAUGHT_HERE,)) | |
494 format_frames(self.frames, w, detail) | |
495 elif not detail == 'brief': | |
496 # Yeah, it's not really a traceback, despite looking like one... | |
497 w("Failure: ") | |
498 | |
499 # postamble, if any | |
500 if not detail == 'brief': | |
501 # Unfortunately, self.type will not be a class object if this | |
502 # Failure was created implicitly from a string exception. | |
503 # qual() doesn't make any sense on a string, so check for this | |
504 # case here and just write out the string if that's what we | |
505 # have. | |
506 if isinstance(self.type, (str, unicode)): | |
507 w(self.type + "\n") | |
508 else: | |
509 w("%s: %s\n" % (reflect.qual(self.type), | |
510 reflect.safe_str(self.value))) | |
511 # chaining | |
512 if isinstance(self.value, Failure): | |
513 # TODO: indentation for chained failures? | |
514 file.write(" (chained Failure)\n") | |
515 self.value.printTraceback(file, elideFrameworkCode, detail) | |
516 if detail == 'verbose': | |
517 w('*--- End of Failure #%d ---\n' % self.count) | |
518 | |
519 def printBriefTraceback(self, file=None, elideFrameworkCode=0): | |
520 """Print a traceback as densely as possible. | |
521 """ | |
522 self.printTraceback(file, elideFrameworkCode, detail='brief') | |
523 | |
524 def printDetailedTraceback(self, file=None, elideFrameworkCode=0): | |
525 """Print a traceback with detailed locals and globals information. | |
526 """ | |
527 self.printTraceback(file, elideFrameworkCode, detail='verbose') | |
528 | |
529 # slyphon: make post-morteming exceptions tweakable | |
530 | |
531 DO_POST_MORTEM = True | |
532 | |
533 def _debuginit(self, exc_value=None, exc_type=None, exc_tb=None, | |
534 Failure__init__=Failure.__init__.im_func): | |
535 if (exc_value, exc_type, exc_tb) == (None, None, None): | |
536 exc = sys.exc_info() | |
537 if not exc[0] == self.__class__ and DO_POST_MORTEM: | |
538 print "Jumping into debugger for post-mortem of exception '%s':" % e
xc[1] | |
539 import pdb | |
540 pdb.post_mortem(exc[2]) | |
541 Failure__init__(self, exc_value, exc_type, exc_tb) | |
542 | |
543 def startDebugMode(): | |
544 """Enable debug hooks for Failures.""" | |
545 Failure.__init__ = _debuginit | |
546 | |
547 | |
548 # Sibling imports - at the bottom and unqualified to avoid unresolvable | |
549 # circularity | |
550 import log | |
OLD | NEW |