Question
Why do virtual subclasses of an abstract Exception created using the ABCMeta.register not match under the except clause?
Background
I'd like to ensure that exceptions that get thrown by a package that I'm using are converted to MyException, so that code which imports my module can catch any exception my module throws using except MyException: instead of except Exception so that they don't have to depend on an implementation detail (the fact that I'm using a third-party package).
Example
To do this, I've tried registering an OtherException as MyException using an abstract base class:
# Tested with python-3.6
from abc import ABC
class MyException(Exception, ABC):
pass
class OtherException(Exception):
"""Other exception I can't change"""
pass
MyException.register(OtherException)
assert issubclass(OtherException, MyException) # passes
try:
raise OtherException("Some OtherException")
except MyException:
print("Caught MyException")
except Exception as e:
print("Caught Exception: {}".format(e))
The assertion passes (as expected), but the exception falls to the second block:
Caught Exception: Some OtherException
Alright, I looked into this some more. The answer is that it's a long-outstanding open issue in Python3 (there since the very first release) and apparently was first reported in 2011. As Guido said in the comments, "I agree it's a bug and should be fixed." Unfortunately, this bug has lingered due to concerns about the performance of the fix and some corner cases that need to be handled.
The core issue is that the exception matching routine PyErr_GivenExceptionMatches in errors.c uses PyType_IsSubtype and not PyObject_IsSubclass. Since types and objects are supposed to be the same in python3, this amounts to a bug.
I've made a PR to python3 that seems to cover all of the issues discussed in the thread, but given the history I'm not super optimistic it's going to get merged soon. We'll see.
The why is easy:
from abc import ABC
class MyException(Exception, ABC):
pass
class OtherException(Exception):
"""Other exception I can't change"""
pass
MyException.register(OtherException)
assert issubclass(OtherException, MyException) # passes
assert OtherException in MyException.__subclasses__() # fails
Edit: This assert mimics the outcome of the except clause, but does not represent what actually happens. Look at the accept answer for an explanation.
The workaround also is easy:
class OtherException(Exception):
pass
class AnotherException(Exception):
pass
MyException = (OtherException, AnotherException)
It seems that CPython once again takes some shortcuts and doesn't bother calling the metaclass's __instancecheck__ method for the classes listed in except clauses.
We can test this by implementing a custom metaclass with __instancecheck__ and __subclasscheck__ methods:
class OtherException(Exception):
pass
class Meta(type):
def __instancecheck__(self, value):
print('instancecheck called')
return True
def __subclasscheck__(self, value):
print('subclasscheck called')
return True
class MyException(Exception, metaclass=Meta):
pass
try:
raise OtherException("Some OtherException")
except MyException:
print("Caught MyException")
except Exception as e:
print("Caught Exception: {}".format(e))
# output:
# Caught Exception: Some OtherException
We can see that the print statements in the metaclass aren't executed.
I don't know if this is intended/documented behavior or not. The closest thing to relevant information I could find was from the exception handling tutorial:
A class in an except clause is compatible with an exception if it is
the same class or a base class thereof
Does that mean that classes have to be real subclasses (i.e. the parent class must be part of the subclass's MRO)? I don't know.
As for a workaround: You can simply make MyException an alias of OtherException.
class OtherException(Exception):
pass
MyException = OtherException
try:
raise OtherException("Some OtherException")
except MyException:
print("Caught MyException")
except Exception as e:
print("Caught Exception: {}".format(e))
# output:
# Caught MyException
In the case that you have to catch multiple different exceptions that don't have a common base class, you can define MyException as a tuple:
MyException = (OtherException, AnotherException)
Well, this doesn't really answer your question directly, but if you're trying to ensure a block of code calls your exception, you could take a different strategy by intercepting with a context manager.
In [78]: class WithException:
...:
...: def __enter__(self):
...: pass
...: def __exit__(self, exc, msg, traceback):
...: if exc is OtherException:
...: raise MyException(msg)
...:
In [79]: with WithException():
...: raise OtherException('aaaaaaarrrrrrggggh')
...:
---------------------------------------------------------------------------
OtherException Traceback (most recent call last)
<ipython-input-79-a0a23168647e> in <module>()
1 with WithException():
----> 2 raise OtherException('aaaaaaarrrrrrggggh')
OtherException: aaaaaaarrrrrrggggh
During handling of the above exception, another exception occurred:
MyException Traceback (most recent call last)
<ipython-input-79-a0a23168647e> in <module>()
1 with WithException():
----> 2 raise OtherException('aaaaaaarrrrrrggggh')
<ipython-input-78-dba8b409a6fd> in __exit__(self, exc, msg, traceback)
5 def __exit__(self, exc, msg, traceback):
6 if exc is OtherException:
----> 7 raise MyException(msg)
8
MyException: aaaaaaarrrrrrggggh
Related
I wrote a library that sometimes raises exceptions. There is an exception that I want to deprecate, and I would like to advise people to stop catching them, and provide advises in the warning message. But how to make an exception emit a DeprecationWarning when catched?
library code
import warnings
class MyException(ValueError):
...
warnings.warn(
"MyException is deprecated and will soon be replaced by `ValueError`.",
DeprecationWarning,
stacklevel=2,
)
...
def something():
raise MyException()
user code
try:
mylib.something()
except MyException: # <-- raise a DeprecationWarning here
pass
How can I modify MyException to achieve this?
You can't. None of the logic that occurs in except MyException is customizable. Particularly, it completely ignores things like __instancecheck__ or __subclasscheck__, so you can't hook into the process of determining whether an exception matches an exception class.
The closest you can get is having the warning happen when a user tries to access your exception class with from yourmodule import MyException or yourmodule.MyException. You can do that with a module __getattr__:
class MyException(ValueError):
...
# access _MyException instead of MyException to avoid warning
# useful if other submodules of a package need to use this exception
# also use _MyException within this file - module __getattr__ won't apply.
_MyException = MyException
del MyException
def __getattr__(name):
if name == 'MyException':
# issue warning
return _MyException
raise AttributeError
Try using this:
import warnings
class MyOtherException(Exception):
pass
class MyException(MyOtherException):
def __init__(self):
warnings.warn(
"MyException is deprecated and will soon be replaced by `MyOtherException`.",
DeprecationWarning,
stacklevel=2,
)
if __name__ == "__main__":
try:
mylib.something()
except Exception:
raise MyException()
I have a custom exception:
class AError(Exception):
def __init__(self, a):
self.a = a
and a function that raises this exception:
def raise_a(self):
raise AError(1)
Using unittest, how do I test that raise_a raises AError with an a == 1? Using:
self.assertRaises(AError, raise_a)
will pass if any instance of AError (such as AError(2)) is raised, but I want it to fail for a != 1.
Using assertRaises as a context manager gives you access to the caught exception:
with self.assertRaises(AError) as cm:
raise_a()
self.assertEqual(cm.exception.a, 1)
I have a situation when unittest method calls a helper method,
if there is a runtime error on the helper method,I wanted to raise sys.exit()
and exit the process without executing any further test case,As sys.exit() is being handled & silenced
by the unittest I had to to use os._exit()
But I don't want to do this for all exception ,only for specific Exceptions in other case I want to raise the Exception as it is.
I have pasted the dummy code of my approach
Please let me know if this a reasonable approach or is there a better way to do this?
class RunTimeError(Exception):
pass
class ExeHandlingDecorator:
def __init__(self,method):
self._method=method
self._clean_up=HelperClass.stop
self._forced_exit=os._exit
self._exit_code=255
def __call__(self,*args,**kwargs):
try:
return self._method(*args,**kwargs)
except RunTimeError as error:
print(error)
self._clean_up()
self._forced_exit(self._exit_code)
except Exception as error:
raise error
class TestDummy(unittest.TestCase):
#classmethod
def setUpClass(cls):
"""setup test"""
#ExeHandlingDecorator
def __helper(self):
try:
"""Testing Environment created here,
RunTimeError might be raised by the called methods in this block"""
finally:
"""Environment clean up done here"""
def test_1(self):
data_to_be_asserted=self.__helper()
I have an exception class that I'm constantly raising with the same messages. This is how I currently have it structured...
class MyException(Exception):
UNAUTHORIZED = 'UNAUTHORIZED'
SOME_OTHER_EXCEPTION = 'SOME_OTHER_EXCEPTION'
SOME_OTHER_THING = 'SOME_OTHER_THING'
# ...
def func():
# ...
raise MyException(MyException.UNAUTHORIZED)
That is quite un-pythonic. I'm wondering if I could do something like this instead...
class MyException(Exception):
UNAUTHORIZED = MyException('UNAUTHORIZED')
SOME_OTHER_EXCEPTION = MyException('SOME_OTHER_EXCEPTION')
SOME_OTHER_THING = MyException('SOME_OTHER_THING')
# ...
def func():
# ...
raise MyException.UNAUTHORIZED
... which looks a lot cleaner.
I'm wondering if this is okay to do... is there any importance where an exception is instantiated?
Or am I approaching this whole thing incorrectly? Should I be doing something like this instead?
class MyException(Exception):
pass
class UnauthorizedException(MyException):
def __init__(self):
super(UnauthorizedException, self).__init__('UNAUTHORIZED')
def func():
# ...
raise UnauthorizedException()
(In this case, I'd have about 10 different custom exception classes, which I think is a bit much, which is why I'm leaning toward my earlier idea.)
In Python 3, exceptions have a __traceback__ attribute storing the corresponding traceback. If you reuse the same exception object everywhere, that's going to overwrite __traceback__ and mess with traceback inspection.
Additionally, reusing exceptions will also mess with exception chaining: __context__ and __cause__. You are likely to see the wrong exceptions reported as the context or cause of reused exceptions.
Don't reuse exception objects. Rather than raise MyException(MyException.UNAUTHORIZED) or raise MyException.UNAUTHORIZED, you should go with the subclasses. It's a lot easier to do
except UnauthorizedException:
...
than it is to do
except MyException as e:
if e.args != (MyException.UNAUTHORIZED,):
raise
...
Is it possible to prevent the initialization of a python object if an exception is catched in __init__?
Example
class pwm():
def __init__(self):
try:
wiring_pi = cdll.LoadLibrary('/home/lib.so')
except:
print "Problem with loading the library:", sys.exc_info()
#DON'T CREATE THE OBJECT, FOR IT IS USELES WITHOUT lib.so
You can raise an exception from __init__, sure. Then the caller will see the exception. Strictly speaking, the object has been created, but it will be reclaimed when the exception is thrown because there are no references to it.
In your example code, either omit the except clause entirely, or raise an exception from it.
Override __new__ and load the dll there. If it fails, raise an exception before the call to super().__new__(...) in __new__.
you can use a static methode who catch the execption in the init and then return None or whatever...
class AesSedai(object):
def __init__(name="Moiraine"):
wiring_pi = cdll.LoadLibrary('/home/lib.so')
#staticmethod
def create_wizard(cls,*args, **kwargs):
try:
wiring_pi = cdll.LoadLibrary('/home/lib.so')
return cls(*args, **kwargs)
except:
print "Problem with loading the library:", sys.exc_info()
return