Search code examples
pythonpython-3.xexceptionpython-descriptors

Exception behaviour in Python __set_name__


I have a child class that is utilising Python 3.6+ __set_name__ to ensure that owning classes have annotated the type of the field carrying the child class. If they haven't an exception is raised.

However, any exception raised is always caught by Python and a RuntimeError raised instead.

For instance:

class Child:
    def __set_name__(self, owner, name):
        raise Exception("OOPS!")

class Owner():
    child = Child()

Results in:

Traceback (most recent call last):
  File "<stdin>", line 3, in __set_name__
Exception: OOPS!

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Error calling __set_name__ on 'Child' instance 'child' in 'Owner'

This may well be expected behaviour (can't find a specific reference to __set_name__ exceptions), but maybe also suggests the expectation is that __set_name__ never suffers exception.

The behaviour I'm seeing isn't a problem, given an exception happens under the right conditions. However, it is tricky to test given I can't be sure the exception raised is the one my code raised.

Is there a better way of raising an exception that will lend itself to testing, or indeed a simple way to check the exception wrapped by the RuntimeError is indeed the one my code raised?


Solution

  • So, since you are getting the whole "The above exception was the direct cause of the following exception" that means that somewhere in type (the base metaclass) there is basically something to the effect of:

    try:
        descr.__set_name__(A, 'attr')
    except Exception as e:
        raise RuntimeError(msg) from e
    

    That is, it's using raise new_exception from original_exception, so you should be able to introspect what the original exception was using the __cause__ attribute:

    So, observe:

    In [1]: class Child:
       ...:     def __set_name__(self, owner, name):
       ...:         raise Exception("OOPS!")
       ...: try:
       ...:     class Owner():
       ...:         child = Child()
       ...: except RuntimeError as e:
       ...:     err = e
       ...:
    
    In [2]: err
    Out[2]: RuntimeError("Error calling __set_name__ on 'Child' instance 'child' in 'Owner'")
    
    In [3]: err.__cause__
    Out[3]: Exception('OOPS!')
    

    Again, I don't think any of this is documented, so you may be relying on an implementation detail.

    Here is a link to the documentation which explains this in more detail.