Currently, when an inlineCallback "yields" a deferred, and that deferred errback's, the yield is thrown the deferred's failure via the defer.py line:
result = g.throw(result.type, result.value, result.tb)
result.tb may be an informative traceback, in which case, yay: no problem exists.
However, result.tb is simply None when the result was errback'd before the addBoth done by inlineCallback (because 'tb' attribute is only kept alive for calls under the stack of the defer.errback(value) call, and this stack includes calls to handlers only if they already were registered when the errback(..) was shot).
I realize that 'tb' is a dangerous thing to keep out of this stack context, but its being None makes the exception raised in the generator have a wrong traceback that contains the yield itself as the source of the exception, rather than the original failure traceback.
It would be useful, if there was a way to map the exception raised by the generator (which, as shown by the quoted defer.py line would be: result.type, result.value) back to "result". This would be useful because then one could use result.frames (via result.printTraceback) to see the interesting information in the original traceback that really caused the problem, rather than just the traceback associated with the "yield" upwards.
There are, however, two problems:
- We cannot provide any extra information besides the exception itself (result.type, result.value) to throw(), it will only throw exception objects.
- We cannot put more information in the exception object itself in the form of attributes (monkey-patching the exception object is ugly at best, and simply invalid at worst).
In order to associate the "result" and its interesting traceback information with the exception, I propose creating a global WeakKeyDictionary mapping in failure.py, that maps exception objects to Failure objects. I don't believe this to be problematic, memory-wise (in weird use-cases, this may cause the livelyhood of a Failure object beyond its current form, if the exception is kept alive, but I think this use-case is both unuseful and not very problematic).
A Failure object that is created for an exception that already had an associated Failure, would remap the exception, while holding a "prev" attribute for the old Failure. This would in effect cause a chain of failures.
This "chain" would allow traceback printers to work correctly, with all traceback information, whereever Python forced a Failure object to be translated into a naked exception object (of which g.throw is a major case).
I have attached an example to show the cases of informative/uninformative tracebacks when using inlineCallbacks.
I also attached a patch draft to failure.py that adds this. It resolves the lack of traceback information issue on the given example.py, but it creates a redundant "chained" traceback when "tb" was not None. I think a simple solution would be to never use the "tb" attribute (i.e: convert the quoted line to g.throw(result.type, result.value, None)).
Another note is that the failure printer I modified, had code to deal with failure chaining (a failure "value" attribute referring to another failure), however this seems impossible, because the Failure __init__ code specifically checks if the value isinstance of Failure, and if so, copies its __dict__, overriding "value" to be an exception object. For now, I just added my "chain" as another chain being printed, but the original chain printing code was never really called, as far as I can see.
Whew, that was long...
What do you think?