msg164184 - (view) |
Author: Tyler Crompton (Tyler.Crompton) |
Date: 2012-06-27 18:27 |
As you know, a caught exception can be re-raised with a simple `raise` statement. Plain and simple. However, one cannot re-raise an error with this new `"from" expression` clause. For example: def getch(prompt=''): '''Get and return a character (similar to `input()`).''' print(prompt, end='') try: return windows_module.getch() except NameError: try: fallback_module.getch() except Exception: raise from None Output: File "getch.py", line 11 raise from None ^ SyntaxError: invalid syntax A quick look at the documentation about [raise](http://docs.python.org/dev/reference/simple_stmts.html#the-raise-statement) confirms that this is the intended behavior. In my opinion, one should be able to still re-raise from an expression. |
|
|
msg164186 - (view) |
Author: Tyler Crompton (Tyler.Crompton) |
Date: 2012-06-27 18:36 |
Relevent PEP: http://www.python.org/dev/peps/pep-0409/ |
|
|
msg164198 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-06-27 20:33 |
I agree that "raise from None" would be a nice enhancement. I don't see it going into 3.3 since we've hit feature freeze. Nick, do we need another PEP, or just consensus on pydev? (If consensus, I can bring it up there after 3.3.0final is released.) |
|
|
msg164204 - (view) |
Author: Alyssa Coghlan (ncoghlan) *  |
Date: 2012-06-27 21:28 |
The from clause is intended for replacing previous exceptions with *new* exceptions, not editing the attributes of existing ones which may already have a different __cause__ set. So - 1 from me, even for 3.4. A patch to the docs explaining that this is not supported syntactically because it risks losing debugging data would be fine, though. |
|
|
msg164205 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-06-27 21:36 |
Okay, I see your point. It's also not difficult to work around if you really want to toss the extra info: except NameError: try: fallback_module.getch() except Exception as exc: raise exc from None A total of three more words to get the desired behavior (and small ones at that). I'll work on a doc patch. |
|
|
msg164206 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-06-27 22:20 |
Patch attached. It basically says: 8<-------------------------------------------------------------------- Note: Because using :keyword:`from` can throw away valuable debugging information, its use with a bare :keyword:`raise` is not supported. If you are trying to do this: try: some_module.not_here() except NameError: try: backup_module.not_here() except NameError: raise from None # suppress context in NameError do this instead: try: some_module.not_here() except NameError: try: backup_module.not_here() except NameError as exc: raise exc from None # suppress context in NameError 8<-------------------------------------------------------------------- |
|
|
msg164208 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-06-27 22:27 |
Nick Coghlan wrote: > The from clause is intended for replacing previous exceptions with *new* > exceptions, not editing the attributes of existing ones which may already > have a different __cause__ set. Huh. While I agree with the doc patch solution, I think the most common use of 'from' will be 'raise SomeException from None' or, as the patch suggests, 'raise exc from None' (exc being an already existing exception). Any examples of when somebody might do: try: do_something() except NameError: raise NewError from SomeOtherError ? I am unsure of the advantages in replacing NameError in the above stack trace with SomeOtherError instead... although NameError would still be there in __context__... Still, any examples? ~Ethan~ |
|
|
msg164214 - (view) |
Author: Alyssa Coghlan (ncoghlan) *  |
Date: 2012-06-28 00:11 |
The purpose of the from clause in general is to change the exception *type* (for example, from KeyError -> AttributeError or vice-versa), or to add additional information without losing access to any previous information (like the original traceback). This is covered in PEP 3134. In some cases, the appropriate way to convey the information is to copy relevant details to the new exception, leave the context set, and suppress display of that context by default. In other cases, the developer may want to display the chained exception explicitly, because it *isn't* part of the current exception handling context (e.g. this is quite likely in an event loop scheduler that passes exception information between events - the __cause__ chain would track the *event stack* rather than the Python call stack). Thus, in the recommended use for the "raise X from Y" syntax, you're always setting __cause__ on a *new* exception, so you know you're not overwriting a preexisting __cause__ and thus there's no risk of losing information that might be relevant for debugging the failure. When you use "raise X from Y" directly on a *pre-existing* exception (however you managed to get hold of it), there's a chance that __cause__ is already set. If you blindly overwrite it, then you've deleted part of the exception chain that PEP 3134 is designed to preserve. This is also why Guido and I were so adamant that just setting __context__ to None was not an acceptable solution for PEP 409 - the whole point of PEP 3144 is to make it difficult to accidentally lose the full details of the traceback even if some of the exception handlers in the stack are written by developers that don't have much experience in debugging other people's code. |
|
|
msg164260 - (view) |
Author: Tyler Crompton (Tyler.Crompton) |
Date: 2012-06-28 14:43 |
I'm in a little over my head as I can't conceptualize __cause__, so I may be looking over things. First, you, Ethan, said the following: >It's also not difficult to work around if you really want to toss the extra info: > > except NameError: > try: > fallback_module.getch() > except Exception as exc: > raise exc from None > >A total of three more words to get the desired behavior (and small ones at that). Counter-argument: if it's just three words, then why was the shorthand without the from clause implemented in the first place? My use case was primarily based on the idea that the unavailability of the windows module (from the example) is irrelevant information to, say, Unix users. When an exception is raised, the user shouldn't have to see any Windows-related exceptions (that is if there is an alternate solution). One could fix this with a little bit of refactoring, though: import sys as _sys def getch(prompt=''): '''Get and return a character (similar to `input()`).''' print(prompt, end='') if 'windows_module' in _sys.modules: return windows_module.getch() else: try: return fallback_module.getch() except Exception: raise from None But it's EAFP. Heck, one could even do the following: def getch(prompt=''): '''Get and return a character (similar to `input()`).''' print(prompt, end='') try: return windows_module.getch() except NameError: pass try: return fallback_module.getch() except Exception: raise But that's not really ideal. I've played around with the traceback module a little and (very) briefly played with the exceptions themselves. Is there not an easier way to suppress a portion of an exception? Like I said, such information is irrelevant to non-Windows users. |
|
|
msg164268 - (view) |
Author: Alyssa Coghlan (ncoghlan) *  |
Date: 2012-06-28 15:59 |
If you don't want the exception context set *at all*, just use a pass statement to get out of the exception before trying the fallback. try: return windows_module.getch() except NameError: pass # No exception chaining fallback_module.getch() |
|
|
msg164269 - (view) |
Author: Alyssa Coghlan (ncoghlan) *  |
Date: 2012-06-28 16:02 |
Correction, your try block is overbroad and will suppress errors in the getch implementation. This is better: try: _getch = windows_module.getch except NameError: _ getch = fallback_module.getch _getch() So I'm sticking with my perspective that wanting to do this is a sign of something else being wrong with the exception handling setup. |
|
|
msg164271 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-06-28 16:08 |
Tyler Crompton wrote: > I'm in a little over my head as I can't conceptualize __cause__, so I may be looking over things. > > First, you, Ethan, said the following: > >> It's also not difficult to work around if you really want to toss the extra info: >> >> except NameError: >> try: >> fallback_module.getch() >> except Exception as exc: >> raise exc from None >> >> A total of three more words to get the desired behavior (and small ones at that). > > Counter-argument: if it's just three words, then why was the shorthand without the from clause implemented in the first place? I'm not sure I understand the question -- do you mean why can we do 'raise' by itself to re-raise an exception? 'from' is new, and was added in Py3k (see below). 'raise', as a shortcut, is there to allow clean-up (or whatever) in the except clause before re-raising the same exception. In 3.0 exceptions were enhanced to include a link to previous exceptions. So if you are handling exception A and exception B occurs, exception B will be raised and will have a link to A. That link is kept in __context__. This complete chain will then be printed if the last exception raised is uncaught. However, there are times when you may want to add more exception information yourself, so we have the `from` clause, which store the extra exception in __cause__. And, there are times when you are changing from one exception to another and do not want the previous one displayed -- so we now have 'from None' (which sets __suppress_context__ to True). So if some underlying function raises ValueError, but you want to transform that to an XyzError, your can do: try: some_function() except ValueError: raise XyzError from None and then, if the exception is uncaught and printed, only the XyzError will be displayed (barring custom print handlers). > My use case was primarily based on the idea that the unavailability of the windows module (from the example) is irrelevant information to, say, Unix users. When an exception is raised, the user shouldn't have to see any Windows-related exceptions (that is if there is an alternate solution). So you are using the absence of the Windows based module as evidence that you are not running on Windows... but what if you are on Windows and there is some other problem with that module? The usual way to code for possible different modules is: try: import windows_module as utils # or whatever name except ImportError: import fallback_module as utils |
|
|
msg164272 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-06-28 16:15 |
Removed sample code from doc patch as it was not robust, nor recommended practice. New patch is effectively: Note: Because using :keyword:`from` can throw away valuable debugging information, its use with a bare :keyword:`raise` is not supported. |
|
|
msg166428 - (view) |
Author: Tyler Crompton (Tyler.Crompton) |
Date: 2012-07-25 21:46 |
As for the losing valuable debug information, much worse can be done: import sys try: x except NameError: print('An error occurred.', file=sys.stderr) sys.exit(1) This is obviously not how one should handle errors in development, but it's allowed. As Ethan pointed out, the initial proposal can be recreated by just adding three words which is obviously also allowed. Nick, I'm not saying you're opinions are wrong, I just wanted to point out how easy it is to throw away valuable information. It's almost futile. |
|
|
msg168704 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-08-20 22:00 |
Any problems with the current doc patch? If not, can it be applied before RC1? |
|
|
msg170339 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-09-11 18:16 |
Can we also get this committed before 3.3.0 final? |
|
|
msg177157 - (view) |
Author: Roundup Robot (python-dev)  |
Date: 2012-12-08 12:24 |
New changeset 8ba3c975775b by Nick Coghlan in branch '3.3': Issue #15209: Clarify exception chaining description http://hg.python.org/cpython/rev/8ba3c975775b New changeset 5854101552c2 by Nick Coghlan in branch 'default': Merge from 3.3 (Issue #15209) http://hg.python.org/cpython/rev/5854101552c2 |
|
|
msg177158 - (view) |
Author: Alyssa Coghlan (ncoghlan) *  |
Date: 2012-12-08 12:27 |
I rewrote the relevant section of the module docs (since they were a bit murky in other ways as well). Since I didn't answer the question earlier, the main reason a bare raise is permitted is because it's designed to be used to a bare except clause (e.g. when rolling back a database transaction as a result of an error). While you could achieve the same thing now with "except BaseException", the requirement for all exceptions to inherit from BaseException is relatively recent - back in the days of string exceptions there was simply no way to catch arbitrary exceptions *and* give them a name. |
|
|
msg177163 - (view) |
Author: Ethan Furman (ethan.furman) *  |
Date: 2012-12-08 16:59 |
There is one typo and one error in the first paragraph of the patch: > When raising a new exception (rather than > using to bare ``raise`` to re-raise the ^ should be an 'a' > exception currently being handled), the > implicit exception chain can be made explicit > by using :keyword:`from` with :keyword:`raise`. > The single argument to :keyword:`from` must be > an exception or ``None``. It will be set as > :attr:`__cause__` on the raised exception. > Setting :attr:`__cause__` also implicitly sets > the :attr:`__suppress_context__` attribute to ``True``. The last sentence is incorrect -- __suppress_context__ is only set to True if __cause__ is set to None; if __cause__ is set to any other exception __suppress_context__ remains False and the new exception chain will be printed: >>> try: ... raise ValueError ... except: ... raise NameError from KeyError ... KeyError The above exception was the direct cause of the following exception: Traceback (most recent call last): File "", line 4, in NameError This is easily fixed by adding 'to ``None``': > Setting :attr:`__cause__` to ``None`` also implicitly sets > the :attr:`__suppress_context__` attribute to ``True``. |
|
|
msg177196 - (view) |
Author: Roundup Robot (python-dev)  |
Date: 2012-12-09 06:22 |
New changeset 3b67247f0bbb by Nick Coghlan in branch '3.3': Issue #15209: Fix typo and some additional wording tweaks http://hg.python.org/cpython/rev/3b67247f0bbb New changeset 04eb89e078b5 by Nick Coghlan in branch 'default': Merge from 3.3 (issue #15209) http://hg.python.org/cpython/rev/04eb89e078b5 |
|
|
msg177197 - (view) |
Author: Alyssa Coghlan (ncoghlan) *  |
Date: 2012-12-09 06:24 |
On Sun, Dec 9, 2012 at 2:59 AM, Ethan Furman <report@bugs.python.org> wrote: > > Ethan Furman added the comment: > > There is one typo and one error in the first paragraph of the patch: > > > When raising a new exception (rather than > > using to bare ``raise`` to re-raise the > ^ should be an 'a' > Fixed. > > Setting :attr:`__cause__` also implicitly sets > > the :attr:`__suppress_context__` attribute to ``True``. > > The last sentence is incorrect -- __suppress_context__ is only set to True > if __cause__ is set to None; if __cause__ is set to any other exception > __suppress_context__ remains False and the new exception chain will be > printed: > > >>> try: > ... raise ValueError > ... except: > ... raise NameError from KeyError > ... > KeyError > > The above exception was the direct cause of the following exception: > > Traceback (most recent call last): > File "", line 4, in > NameError > Not true: __suppress_context__ is always set as a side effect of setting __cause__ (it's built into the setter for the __cause__ descriptor). What you're seeing in the traceback above is the explicit cause, not the implicit context. >>> e = Exception() >>> e.__cause__ = Exception() >>> e.__suppress_context__ True The only mechanism we offer to suppress an explicit __cause__ is setting __cause__ to None. Cheers, Nick. |
|
|