Saturday, January 8, 2022

Brett Cannon: Unravelling `from` for `raise` statements

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:

  1. Assigning an exception instance is fine.
  2. Assigning a non-exception-related object is "fine" because the exception object will raise the exception.
  3. 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:

  1. If we have an exception class, instantiate it.
  2. If we have a non-exception object for the raise clause, raise TypeError.
  3. Rely on the fact that assigning anything other than an exception instance to __cause__ raises the appropriate TypeError 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

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...