I want to save exception in one function, and raise it on upper levels of call stack, but it's not possible. Saving exception with try/except clause doesn't save traceback
Next code:
def saving_exception() -> SyntaxError:
# try:
raise SyntaxError('Exception')
# except SyntaxError as e:
# return e
def middle_function() -> SyntaxError:
exception = saving_exception()
return exception
if __name__ == '__main__':
raise middle_function()
Returns 3 frames:
<module>
middle_function
saving_exception
Uncommenting 3 lines in saving_exception() will return 2 frames:
<module>
saving_exception
Also return e.with_traceback(sys.exc_info()[2]) in saving_exception() do the same as return e
How to make saving_exception() return exception with 3 frames without big changes outside?
Related
So I have say 3 functions I would like to run. Each of them can fail. I would like to built try-except around them to:
let as many as possible out of 3 run AND
raise an error at the end if any of them failed. Is this possible?
The below code fails as operation D (in the middle fails) so E is never reached:
try:
c = 3 + 6
print(c)
except TypeError:
raise TypeError("Wrong type provided in operation C")
try:
d = 3 + '6'
print(d)
except TypeError:
raise TypeError("Wrong type provided in operation D")
try:
e = 7 + 5
print(e)
except TypeError:
raise TypeError("Wrong type provided in operation E")
def f1():
print("f1")
def f2():
raise TypeError
print("f2")
def f3():
print("f3")
err = None
for f in [f1, f2, f3]:
try:
f()
except TypeError as e:
# store first error
if not err:
err = e
if err:
raise err
Output:
f1
f3
[...]
TypeError
If your functions take arguments, you can loop over
[(f1, f1_args, f1_kwargs), (f2, f2_args, f2_kwargs), (f3, f3_args, f3_kwargs)]
Inspired by a comment I tried to come up with a nifty context manager that sort of raises all the exceptions in a nested fashion upon exiting. Comments are welcome.
class ErrorCollector:
def __init__(self):
self.errors = []
def exec(self, f, suppress_types=None, *args, **kwargs):
suppress_types = tuple(suppress_types) if suppress_types else ()
try:
return f(*args, **kwargs)
except suppress_types as e:
self.errors.append(e)
def _raise_all(self, errors):
if len(errors) == 1:
raise errors[0]
for e in errors:
try:
raise e
except type(e):
self._raise_all(errors[1:])
def __enter__(self):
return self
def __exit__(self, exctype, excinst, exctb):
if excinst is not None:
self.errors.append(excinst)
self._raise_all(self.errors)
def f1():
print('f1')
def f2():
raise TypeError('TypeError in f2')
print('f2')
def f3():
raise ValueError('ValueError in f3')
print('f3')
def f4():
raise TypeError('TypeError in f4')
print('f4')
def f5():
print('f5')
def f6():
raise ZeroDivisionError('ZeroDivisionError in f6')
print('f6')
def f7():
print('f7')
Now you can use:
suppress = [TypeError, ValueError]
with ErrorCollector() as ec:
for f in (f1, f2, f3, f4, f5, f6, f7):
ec.exec(f, suppress)
with the output:
f1
f5
[...]
TypeError: TypeError in f2
During handling of the above exception, another exception occurred:
[...]
ValueError: ValueError in f3
During handling of the above exception, another exception occurred:
[...]
TypeError: TypeError in f4
During handling of the above exception, another exception occurred:
[...]
ZeroDivisionError: ZeroDivisionError in f6
Note that f7 is not executed because the ZeroDivisionError was not suppressed.
Other responses have provided ad-hoc handlers of various manners. An interesting alternative if this is a recurring need is to build a context manager similar to contextlib.suppress, something along the lines of (untested):
class Suppressor:
def __init__(self, *to_suppress):
self.to_suppress = to_suppress # empty = suppress all
self.exceptions = []
def __enter__(self):
pass
def __exit__(self, etype, val, _):
if etype is not None:
# if the exception is selected
if not self.to_suppress or isinstance(val, self.to_suppress):
# store and suppress it
self.exceptions.append(val)
return True
# otherwise ignore
Usage:
suppressor = Suppressor(TypeError)
with suppressor:
c = 3 + 6
print(c) # 9
with suppressor:
d = 3 + '6' # error (suppressed)
print(d)
with suppressor:
e = 7 + 5
print(e) # 12
if suppressor.exceptions:
# TypeError("unsupported operand type(s) for +: 'int' and 'str'")
print(repr(suppressor.exceptions[0]))
with suppressor:
# ValueError: invalid literal for int() with base 10: 'a'
e = 7 + int('a')
The most simple would be:
errors = 0
try:
c = 3 + 6
print(c)
except TypeError:
errors +=1
try:
d = 3 + '6'
print(d)
except TypeError:
errors +=1
try:
e = 7 + 5
print(e)
except TypeError:
errors +=1
if errors!=0:
raise TypeError("Wrong type provided in some operation")
If you want to get which operation threw TypeError you can assign it in 3 different variable.
Hope this helps!
You could collect the exceptions. Then, you can actually reraise them rather than building a new exception, by having access to each.
exceptions = []
try:
raise ValueError("thing 1 error") # simulate error in first function
except ValueError as exc:
exceptions.append(exc)
try:
raise ValueError("thing 2 error") # simulate error in second function
except ValueError as exc:
exceptions.append(exc)
try:
raise ValueError("thing 3 error") # simulate error in third function
except ValueError as exc:
exceptions.append(exc)
You can then optionally raise a new exception. Something like:
if exceptions:
raise Exception("something went wrong")
Which gives:
Traceback (most recent call last):
File "./main.py", line 21, in <module>
raise Exception("something went wrong")
Exception: something went wrong
Or, you can directly access any or all of these exceptions. Something like:
raise exceptions[1]
Which gives:
Traceback (most recent call last):
File "./main.py", line 25, in <module>
raise exceptions[1]
File "./main.py", line 11, in <module>
raise ValueError("thing 2 error") # simulate error in second function
ValueError: thing 2 error
Problem
Cannot catch a StopIteration raised from within a iterator.
Example
from typing import (
Generator,
Iterable,
List
)
import sys
import pathlib
from itertools import islice
import numpy as np
def take(n: int, iterable: Iterable) -> List:
taken = list(islice(iterable, 0, n))
if len(taken) > 0:
return taken
else:
print("Raising StopIteration")
raise StopIteration("Nothing to take") # <----- Raise StopIteration
def file_line_stream(path: str) -> Generator[str, None, None]:
if not pathlib.Path(path).is_file():
raise FileNotFoundError(f"file {path} does not exist or non file")
try:
_file = pathlib.Path(path)
with _file.open() as f:
for line in f:
yield line.rstrip()
except (IOError, FileNotFoundError) as e:
raise RuntimeError("Read file failure") from e
def get_sentences(path_to_file, num_sentences):
counter = 0
stream = file_line_stream(path_to_file)
try:
while True:
_lines = take(num_sentences, stream)
print(f"Count {counter}: yielding [{_lines[0]}]\n")
yield np.array(_lines)
counter += 1
finally:
stream.close()
def test():
path_to_input = "test_has_two_lines.txt"
generator = get_sentences(path_to_file=path_to_input, num_sentences=100)
for i in range(10):
# ----------------------------------------------------------------------
# Want to catch the StopIteration raised from within the generator.
# ----------------------------------------------------------------------
try:
next(generator)
except StopIteration as e:
print("Caught at StopIteration except")
generator.close()
except Exception as e: # <--- Raised StopIteration will get caught here
print("Caught at catch-all except: %s", sys.exc_info()[0])
generator.close()
raise e
if __name__ == '__main__':
test()
Result
Count 0: yielding [previous monthly <unk> of the major market index futures and standard & poor 's <unk> index options have produced spectacular volatility]
Raising StopIteration
Caught at catch-all except: %s <class 'RuntimeError'>
Traceback (most recent call last):
File "test_050_word2vec.py", line 39, in get_sentences
_lines = take(num_sentences, stream)
File "test_050_word2vec.py", line 19, in take
raise StopIteration("Nothing to take") # <----- Raise StopIteration
StopIteration: Nothing to take
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test_050_word2vec.py", line 67, in <module>
test()
File "test_050_word2vec.py", line 63, in test
raise e
File "test_050_word2vec.py", line 55, in test
next(generator)
RuntimeError: generator raised StopIteration <----- Converted into RuntimeError
Cause
exception StopIteration
Changed in version 3.7: Enable PEP 479 for all code by default: a StopIteration error raised in a generator is transformed into a RuntimeError.
Reason
PEP 479 -- Change StopIteration handling inside generators
Currently, StopIteration raised accidentally inside a generator function will be interpreted as the end of the iteration by the loop construct driving the generator.
Don't raise StopIteration, raise something sane like ValueError
StopIteration serves a very specific purpose (to allow a next method to indicate iteration is complete), and reusing it for other purposes will cause problems, as you've seen.
The conversion to RuntimeError here is saving you; if Python hadn't done that, the generator would have silently stopped iterating (StopIteration is swallowed silently, causing iteration to end without propagating the exception; you'd never catch it anyway).
I am having issues with a ValueError working the way I want.
I have a function that is returning a string but I do not want it too evaluate to a ValueError if it is not raised from the function
Sample Code
def test(a):
if a == a:
raise ValueError('There was a error # 2')
a = 'a'
if ValueError:
print "There was a error # 1"
test(a)
Output
There was a error # 1
Traceback (most recent call last):
File "/home/user/Test_1.py", line 13, in <module>
test(a)
File "/home/user/Test_1.py", line 5, in test
raise ValueError('There was a error # 2')
ValueError: There was a error # 2
Process finished with exit code 1
If I read the docs correctly it said it can be raised by a string, how do i prevent this behavior?
https://docs.python.org/2/library/exceptions.html#exceptions.IndexError
Not sure why it was working before but I made it more explicit and it works now. Also the first example was more vague and I was trying to catch error from a function that was in the library.
Sample Code
def test(a):
try:
if a == a:
pass
raise ValueError('There was a error # 2')
except Exception, e:
str(e)
return e
a = 'a'
b = test(a)
if type(b) == ValueError:
print b
Output
There was a error # 2
Process finished with exit code 0
Why can't I raise an Exception instance after I catch that Exception class? Oddly enough, I run into this error when I run the script from a function but not when ran directly in the python shell.
In [2]: def do():
...: try:
...: raise ValueError('yofoo')
...: except TypeError, ValueError:
...: raise ValueError('yo')
...:
In [3]: do()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-3-30c46b84d9a4> in <module>()
----> 1 do()
<ipython-input-2-b62158d6343b> in do()
1 def do():
2 try:
----> 3 raise ValueError('yofoo')
4 except TypeError, ValueError:
5 raise ValueError('yo')
UnboundLocalError: local variable 'ValueError' referenced before assignment
Expected error here:
In [3]: try:
...: raise ValueError("foo")
...: except ValueError:
...: raise ValueError("bar")
...:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-3-d5c83419a5ea> in <module>()
2 raise ValueError("foo")
3 except ValueError:
----> 4 raise ValueError("bar")
5
ValueError: bar
except TypeError, ValueError:
should be
except (TypeError, ValueError):
When you use except TypeError, ValueError:, you are assigning the Exception instance to the variable name ValueError.
With your current code, when Python parses the do function, it notes ValueError is a local variable because except TypeError, ValueError: assigns a value to ValueError. But when you reference it in the first try-suite, with raise ValueError('yofoo'), the local variable ValueError has no value. So you get UnboundLocalError.
From the docs:
... except (RuntimeError, TypeError, NameError):
... pass
Note that the parentheses around this tuple are required, because
except ValueError, e: was the syntax used for what is normally written
as except ValueError as e: in modern Python (described below). The old
syntax is still supported for backwards compatibility. This means
except RuntimeError, TypeError is not equivalent to except
(RuntimeError, TypeError): but to except RuntimeError as TypeError:
which is not what you want.
I know that if I want to re-raise an exception, I simple use raise without arguments in the respective except block. But given a nested expression like
try:
something()
except SomeError as e:
try:
plan_B()
except AlsoFailsError:
raise e # I'd like to raise the SomeError as if plan_B()
# didn't raise the AlsoFailsError
how can I re-raise the SomeError without breaking the stack trace? raise alone would in this case re-raise the more recent AlsoFailsError. Or how could I refactor my code to avoid this issue?
As of Python 3, the traceback is stored in the exception, so a simple raise e will do the (mostly) right thing:
try:
something()
except SomeError as e:
try:
plan_B()
except AlsoFailsError:
raise e # or raise e from None - see below
The traceback produced will include an additional notice that SomeError occurred while handling AlsoFailsError (because of raise e being inside except AlsoFailsError). This is misleading because what actually happened is the other way around - we encountered AlsoFailsError, and handled it, while trying to recover from SomeError. To obtain a traceback that doesn't include AlsoFailsError, replace raise e with raise e from None.
In Python 2 you'd store the exception type, value, and traceback in local variables and use the three-argument form of raise:
try:
something()
except SomeError:
t, v, tb = sys.exc_info()
try:
plan_B()
except AlsoFailsError:
raise t, v, tb
Even if the accepted solution is right, it's good to point to the Six library which has a Python 2+3 solution, using six.reraise.
six.reraise(exc_type, exc_value, exc_traceback=None)
Reraise an exception, possibly with a different traceback.
[...]
So, you can write:
import six
try:
something()
except SomeError:
t, v, tb = sys.exc_info()
try:
plan_B()
except AlsoFailsError:
six.reraise(t, v, tb)
As per Drew McGowen's suggestion, but taking care of a general case (where a return value s is present), here's an alternative to user4815162342's answer:
try:
s = something()
except SomeError as e:
def wrapped_plan_B():
try:
return False, plan_B()
except:
return True, None
failed, s = wrapped_plan_B()
if failed:
raise
Python 3.5+ attaches the traceback information to the error anyway, so it's no longer necessary to save it separately.
>>> def f():
... try:
... raise SyntaxError
... except Exception as e:
... err = e
... try:
... raise AttributeError
... except Exception as e1:
... raise err from None
>>> f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in f
File "<stdin>", line 3, in f
SyntaxError: None
>>>