I would like to handle a type of exception, but only in the case that it came chained from another specific type of exception. I am confused on how it should be done.
Example:
def raise_valueerr_from_oserr():
try:
raise PermissionError("Booo!")
except OSError as _:
raise ValueError('Aaaaah!') from _
try:
raise_valueerr_from_oserr()
except ValueError as _:
if isinstance(_.__cause__, OSError): # Here
print('ValueError from OSError')
exit(1)
raise _
Question: Should I use __cause__, or __context__, or something else?
I am not entirely sure if I understood the implicit vs explicit in here. Let’s see if I got it. If I am sure they did from _ in raise_valueerr_from_oserr, then looking at __cause__ is enough, but if not sure I should look in __context__.
The exception that happens when logging cannot write to LOG_FILE is a ValueError chained from a PermissionError.
I wanted to handle this, to inform that the environment variable LOG_FILE should be changed.
I didn’t want to handle all ValueError coming from this portion of code, to not obscure any other potential problem being raised with that same type of exception.
If you raise X from Y then __cause__ will be set (and will be equal to __context__). If you simply do raise X then __context__ will be set, but __cause__ won’t be.
You can verify by tinkering with your code:
def absurd(chain=False):
try:
assert 0 == 1
except Exception as e:
if chain:
raise ValueError('chained') from e
else:
raise ValueError('unchained')
def catch_absurdities(chain=False):
try:
absurd(chain=chain)
except Exception as e:
if chain:
raise RuntimeError(f'caught chained = {chain}') from e
else:
raise RuntimeError(f'caught chained = {chain}')
def test(chain=False):
try:
catch_absurdities(chain)
except Exception as e:
return e
e1 = test(False)
print(e1)
assert e1.__context__.__class__ is ValueError
assert e1.__context__.__context__.__class__ is AssertionError
assert e1.__cause__ is None
assert e1.__context__.__cause__ is None
e2 = test(True)
print(e2)
assert e2.__context__.__class__ is ValueError
assert e2.__context__.__context__.__class__ is AssertionError
assert e2.__cause__ is e2.__context__
assert e2.__cause__.__cause__ is e2.__context__.__context__
In my case the code raising the exception is not mine.
Then, I suppose the cautious way would be to inspect __context__.
Another thing that I need to consider is if I care about the depth of when the OSError got chained or not,
check _.__context__ vs _.__context__.__context__, … etc.
I have a feeling that you want to/are trying to do sth that is too complicated for its own good. Of course I don’t know what that “good” would be, but I’ve never had an occassion where I needed to handle this kind of deeply nested exceptions. The default option in Python scripts is also to “just let it come down”, let the exceptions be thrown, don’t catch any, especially if they’re coming from another library. In this case, are you sure your code would be able to actually handle the special exception?
Ok… So, there are always multiple ways to try to prevent or to deal with these kind of mishaps.
If you know in advance that there may be permission issues with the log file, then you could
explicitly check for that as soon as that variable is defined. (This is what I would do, since you then stays as close as possible to the fracture and don’t have to complicate the rest of the code.)
Otherwise, if you catch the potential ValueError exc of logging.dictConfig, then you could distinguish the case were exc.__cause__ is set and is a PermissionError (cause will be set in that case) from other situations. In that case exc.__cause__ (I just found out) also has a exc.__cause__.filename2 attribute that contains the path to the filename. So, you could do sth like:
try:
dictConfig(...)
except ValueError as exc:
cause = exc.__cause__
if cause is None: # don't know what to do
raise
if not isinstance(cause, PermissionError): # same
raise
print(f"PermissionError: Can not write to {cause.filename2}")
# could investigate futher and see why not, or just guess
# that it's a non-writable folder or file
print("Please make that file writable.")
sys.exit(1)
If code will be used by other people (or on some organization’s server etc), try to ensure that
all external data (env variables, file paths) as defined by a user will be vetted before it is actually used in the code.
As to
I have seen the advice to not go this route, since things might change from when I check myself, to when logging tries to access the file.
Really, that should be seen as a bug. Constants should remain constant
What they meant in the advice was that a file could be readable one time, and not at some other time; the drive could disconnect, or some other process could modify it, things like that.