As part of my series on Python&aposs syntax, I want to tackle the from
clause for raise
statements. In case you&aposre unfamiliar, raise A from B
causes B
to be assigned to A.__cause__
which lets chained tracebacks exist (as well as __context__
, but that&aposs not relevant to today&aposs topic). There is a restriction that only instances of exceptions can be assigned to __cause__
, and if you specify an exception class then it gets instantiated before assignment. Trying to assign anything else triggers a TypeError
.
So the first question is how much checking do we need to do for the from
clause&aposs object to make sure it meets the requirement of being an (eventual) exception instance?
>>> exc1 = Exception()
>>> exc1.__cause__ = 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: exception cause must be None or derive from BaseException
>>> exc1.__cause__ = Exception
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: exception cause must be None or derive from BaseException
>>> exc1.__cause__ = Exception()
>>>
Exploring restrictions when assigning to BaseException.__cause__
It appears that only assigning an instance of an exception to __cause__
is allowed. This is convenient as we don&apost have to do the check for something not being an exception instance. But it&aposs also inconvenient as we do have to instantiate any exception class ourselves. What this says to me is the following:
- Assigning an exception instance is fine.
- Assigning a non-exception-related object is "fine" because the exception object will raise the exception.
- Assigning an exception class is problematic, and so we will have to handle the instantiation.
So how do we detect if an object is an exception class but not an instance? To tell if an object is a class, it needs to be an instance of type
(as inspect.issclass()
shows us); isinstance(obj, type)
. To tell if something is an exception instance, it needs to be an instance of BaseException
; isinstance(obj, BaseException)
. And to tell if a class is an exception class, we need to know if one of its subclasses is BaseException
; issubclass(obj, BaseException)
. Now one thing to note is issubclass()
will raise a TypeError
if you pass anything that isn&apost a class as its first argument; isinstance()
does not have the inverse issue of passing in a class as its first argument.
Oh, and we also have to make sure the object we are raising is also appropriately instantiated before we attempt any assignment to __cause__
. That adds an extra wrinkle to this problem as it means we will have to raise the TypeError
when the to-be-raised exception isn&apost an exception-related object.
This can all be summarized by the following function:
def exception_instance(obj):
if isinstance(obj, BaseException):
return obj
elif isinstance(obj, type) and issubclass(obj, BaseException):
return obj()
else:
raise TypeError("exceptions must derive from BaseException")
Utility function for getting an exception instance
When we unravel raise A from B
, we can inline the logic and simplify it a bit:
- If we have an exception class, instantiate it.
- If we have a non-exception object for the
raise
clause, raiseTypeError
. - Rely on the fact that assigning anything other than an exception instance to
__cause__
raises the appropriateTypeError
for us.
This lets us unravel raise A from B
to:
_raise = A
if isinstance(_raise, type) and issubclass(_raise, BaseException):
_raise = _raise()
elif not isinstance(_raise, BaseException):
raise TypeError("exceptions must derive from BaseException")
_from = B
if isinstance(_from, type) and issubclass(_from, BaseException):
_from = _from()
_raise.__cause__ = _from
raise _raise
Unravelling raise A from B
In the general case of raise A
we don&apost have to do any of this as it&aposs part of Python&aposs semantics to handle the class-to-instance scenario.
from Planet Python
via read more
No comments:
Post a Comment