-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
TracebackException or StackSummary.extract with capture_locals=True fail to catch exceptions raised by repr() on value of frame local variable in FrameSummary.__init__. #87822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
With Line 440 in 4827483
This will fail, however, if string conversion fails. StackSummary.format should be robust towards such possibilities. An easy fix would be a utility function:
|
I have to correct myself: The conversion to string already happens during construction, in Line 273 in 4827483
The issue remains as severe and the fix remains as easy. |
Why str() is called at all? Would not be better to use repr()? |
Yes, it is repr in FrameSummary.__init__. |
See bpo-39228. |
I didn't know repr is supposed to always succeed. Does the documentation mention this? |
I can't find any mention of this in the documentation: https://docs.python.org/3/reference/datamodel.html#object.\_\_repr__ https://docs.python.org/3/library/functions.html#repr I think this should be mentioned somewhere. |
I think it's the other way around - the documentation usually states what exceptions a function can raise as part of its published API, not which exceptions it should not raise. |
@martin it's more that logic in a function may raise exception and it will be handled somewhere upstream, but a repr should just give you a simple representation of an object, it would make no sense for upstream logic to be "object repr failed with such exception, instead we should do this now". It's hard to imagine when this would make sense. |
Thanks for the explanations. I think this issue can be closed then. |
I would like to request that this bug be repopened and fixed. I've changed (or at least tried to change, I'm not sure if it will let me) the title of the bug to point out that the failure happens in FrameSummary.__init__. It does not happen in StackSummary.format. This problem makes the capture_locals=True feature of TracebackException and StackSummary and the "locals" parameter of FrameSummary unreliable. If any one of the local variables in any frame on the stack is in an inconsistent state such that repr will raise an exception, the processing of the traceback fails. This kind of inconsistent state is of course likely to happen during debugging, which is precisely when you would want the capture_locals feature to actually work so you can see what is going wrong. Just one example of an exception traceback being created with an unsafe local variable value in one of the stack frames is in the following line: from _pydecimal import Decimal as D; D(None) The _pydecimal.Decimal.__new__ method raises an exception when it sees a value it doesn't know how to convert to Decimal. When it does this, the new object it was creating is left in an inconsistent state missing the _sign attribute. When you try to inspect the resulting exception traceback with traceback.TracebackException(..., capture_locals=True), this raises an exception. While I was tracking this bug down, I discovered that the traceback.TracebackException.__init__ method has the same problem: it initializes the _str attribute used by the __str__ method quite late and when it raised an exception before this point, the incompletely initialized TracebackException object caused repr to fail. There's at least one more class in traceback.py that also has this problem, but I don't remember which one it is. The cascade of exceptions causing exceptions causing exceptions makes the capture_locals feature difficult to use and debug. Here is a short snippet that reproduces the bug on Python 3.9.7: import traceback
class TriggerTracebackBug:
def __init__(self):
raise RuntimeError("can't build a TriggerTracebackBug object for some reason")
self._repr = 'if we reached this line, this object would have a repr result'
def __repr__(self):
return self._repr
try:
TriggerTracebackBug()
except Exception as e:
# this method call fails because there is a stack frame with a local
# variable (self) such that repr fails on that variable's value:
traceback.TracebackException.from_exception(e, capture_locals=True) It's clear that it is too much to expect every class to implement a safe __repr__ method. So it also seems clear to me that traceback.FrameSummary.__init__ is the place to fix it. My suggested fix is to replace this line in the latest Lib/traceback.py: self.locals = {k: repr(v) for k, v in locals.items()} if locals else None with something like this code (written in a somewhat awkward way because that helped while I was debugging it): if locals:
d = {}
self.locals = d
for k, v in locals.items():
try:
d[k] = repr(v)
except Exception:
d[k] = '''<an exception was raised while trying to format this local variable's value>'''
else:
self.locals = None I've tested this code in an older version of Python and it fixed the problem for me. |
Irit, I'm unsure about the wording. Something like ":meth:`__repr__` should always succeed, even if errors prevent a full description of the object."? "... even if the object is only partly initialized."? Andrei, I think you can not simply omit the first argument. For virtually all methods, I really think one important part of the solution to this problem is making people aware, that under some rare conditions, repr might receive a partly initialized object. I'm also still convinced that the other part of the solution is a way to customize the behavior of StackSummary.extract and friends. In the end, this whole problem only affects users of |
Martin, how about something like: "This is typically used for debugging, so it is important that the representation is information-rich and unambiguous. Furthermore, this function should not raise exceptions because that can make it inappropriate for use in some debugging contexts. " |
Martin: It's true that exceptions raised in other methods called from __init__ would still have this issue, but I feel like ~90% of it would be solved by the proposed fix. It does introduce an inconsistency but it does so because it reflects the inconsistency of The doc fix is useful in itself, but I'm not sure it's enough given the issue reported by Joe with new students getting this error. When they get this error, they will not understand why it happens, what's the connection between the error and the argument that needs to be provided to fix it, and whether it's the correct fix or not. If they *do* understand it, fixing the __repr__ is probably the best solution, and then no fix on our side is required. My concern with adding a safe repr argument is that code using that argument can be copy pasted unknowingly and then it's not explicit/obvious that it will silence the errors raised due to broken __repr__'s. (including __repr__'s that are broken even outside of __init__ method). If such an parameter is added, it's much better to make it a new parameter, to avoid backwards incompatible change, which means it can be backported. |
|
Once again a very good summary, thanks Joe!
So this would be something like I wouldn't bother with detecting un-repr-able objects in Python itself, but if the above What is needed to get an OK for such a change? |
Could the participants of this issue please have a look at my pull request: What do you like, what don't you like? Does it work for your use case? |
Martin: I'm not sure what is the best way to fix this issue, so I hope someone else will look into this. |
I see two scenarious discussed here: Scenario 1 (Offline / Batch-Mode): This could be a a system to automatically test the homework code of your students. Or, like in my case, a framework that runs experiments. You would very likely want a system that does not crash if a __repr__ is wrongly implemented but prints a readable traceback and carries on with the next job. Here, format_locals could be used to fall back to a different representation of an object if repr() fails. This is the use case that my pull request tries to address primarily. Scenario 2 (Online / Write, Test, Repeat): Yes, it would be hard for unexperienced users to come up with StackSummary.extract(format_locals=...) by themselves. However, if your students receive a template file where they fill in missing function bodies etc, there might already be a substantial amount of code which they don't understand at first, nor do they need to care. Therefore, a piece of code that formats exceptions nicely would hurt here. I think the most useful change for Scenario 2 (if, for some reason, a proper debugger is not an option) could be to add a command line option (e.g. -X capture_locals) so that a complete stack trace is printed if an exception bubbles up to interpreter level. (Here, it wouldn't hurt to handle exceptions in repr() because the interpreter already stopped exection anyway.) My pull request would provide preparation for this more extensive change. |
Martin: I have a couple of concerns:
In this case of course arbitrary exceptions coming any objects' __repr__ may be silenced. There is a clear and very explicit way to catch exceptions via try/except and as a dev, I would really want to be able to look at a module, look at all try/except clauses and be confident that exceptions are not silenced elsewhere.
IOW, as a dev I might prefer to see these errors early and often, rather than have a mechanism that ends up silencing errors more broadly than intended. I'm not saying this fix should be rejected, but that there's a tricky balance here -- and I don't feel confident enough about this solution to approve the PR if I reviewed it. |
Just to avoid misunderstandings: My pull request is not at all about silencing exceptions. It is about customizing the output of the traceback module. (Just like the introduction of capture_locals previously: bpo-22936) (-X capture_locals, on the other hand, is a hypothetical idea that might help with debugging, but we don't have to discuss this now.) My main argument for the proposed change is that traceback is useless in certain situations, because capture_locals is not "robust" (it *might* raise exceptions; I could handle these in the calling code but that would again hide the stack trace that I was about to investigate) and might dump sensitive data like passwords into logfiles (msg237320, msg237323). Nothing is taken away or hidden, but flexibility is added. (The only other option to resolve these issues would be to re-implement much of the current functionality of traceback in a third-party module, which would lead to maintainability problems and fragmentation.) |
I improved the example in traceback.rst to illustrate how format_locals works. |
Some quick comments on Martin's pull request.
|
Thanks Joe!
That was my goal :)
I agree and already updated the pull request.
I think format_locals is OK here (local*s*: plural), but I'm open to better suggestions.
I sort of agree, but I also don't know what such a general-purpose function would entail. Suggestions?
I agree! In principle, FrameSummary has been little more than a container without (much) logic of its own.
Do you have a use case for that? I have no clue what you would do with the frame object at this point.
Previously, I totally misread your request to "not just the local variable values, but also the name of the local variable and maybe also the stack frame it is in". This is why I included filename, lineno and name as parameters to format_locals which now seems totally useless to me and I'll remove them (if nobody objects). |
Irit, would you be able to take a look at the patch? --- I found another application scenario for the patch: In Numpy and Pandas, the assert_* functions in testing have a __tracebackhide__ local that advises pytest to hide the frame. With the patch in place, we could at least not display any locals if __tracebackhide__ is present. |
While I do think this module should be more customisable than it is, I think it should be done via support for subclassing rather than injecting functions that get forwarded on as you do here. There are other issues on bpo related to customising this module, with more convincing use cases than suppressing errors. I think it’s more likely that a patch will be accepted which solves the general problem of this module being inflexible (while being backwards compatible). The patch you propose here solves a small part of the problem while making the api clunkier and it commits us to supporting this new parameter when we try to solve the general problem. |
Thanks for your definitive answer, this is what I was waiting for. I understand and I totally agree that subclassing is the way to go to make traceback more flexible. Would you mind linking the other issues concerning the general improvement of traceback? |
Try the search function on the tracker (that's what I would need to do to find what to link). |
While I don't think it's a good idea to teach python learners that swallowing exceptions is a good idea, and it's easy enough to rerun the student's homework with capture_locals=False, the situation may be different in large prod jobs that fail intermittently. In a case like that the problem may not be easily repeatable, and the traceback module should prioritise the original exception over any rendering errors. While it is a little weird to use capture_locals in a production job, people may want to do it. Given that we have the _safe_string utility which is used in a few other places for this purpose (for either repr or str), I don't think there is much harm applying it to the locals as well. @moi90 - Would you like to submit a path with this change, plus some unit tests and doc update? This is a new feature rather than a bugfix so we should not backport it.
|
Like this? :) |
Yes, I suggested a rewording of the news file. You can click the button to accept it if you're happy with it. |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: