Running `python -m unittest` changes the way exceptions with overriden `__name__` are printed in the traceback

Viewed 61

I have a piece of code that dynamically creates Exceptions. Every exception class created in this way gets its __name__ overwritten:

def exception_injector(name, parent, module_dict):
    class product_exception(parent):
        pass
    product_exception.__name__ = name
    product_exception.__module__ = module_dict["__name__"]
    module_dict[name] = product_exception

When I use it regularly it prints everything out just fine:

>>> namespace = {"__name__": "some.module"}
>>> exception_injector("TestError", Exception, namespace)
>>> raise namespace["TestError"]("What's going on?")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
some.module.TestError: What's going on?

But when I use the unittest module the original __name__ is printed:

>>> import unittest
>>> class DemoTestCase(unittest.TestCase):
...     def test_raise(self):
...         namespace = {"__name__": "some.module"}
...         exception_injector("TestError", Exception, namespace)
...         namespace["TestError"]("What's going on?")
...
>>> unittest.main(defaultTest="DemoTestCase", argv=["demo"], exit=False)
E
======================================================================
ERROR: test_raise (__main__.DemoTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 3, in test_raise
some.module.exception_injector.<locals>.product_exception: What's going on?

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)
<unittest.main.TestProgram object at 0x1017c3eb8>

From where on the exception object could the unittest module be getting this original information?

1 Answers

Unittests use the traceback module to format exceptions. You can reproduce your output by doing the same:

>>> import traceback
>>> try:
...     raise namespace["TestError"]("What's going on?")
... except Exception as e:
...     tb = traceback.TracebackException.from_exception(e)
...     print(*tb.format())
...
Traceback (most recent call last):
   File "<stdin>", line 2, in <module>
 some.module.exception_injector.<locals>.product_exception: What's going on?

What is being printed is the object __module__ attribute value*, with object.__qualname__ attribute appended (with a dot separator), rather than __module__ plus __name__:

>>> namespace["TestError"].__qualname__
'exception_injector.<locals>.product_exception'

The qualified name includes the full scope where the class was created (function names here, but this could also include class names).

If your aim is to add exceptions to the global namespaces of a module, you can just set it to the same value as name:

>>> namespace["TestError"].__qualname__ = "TestError"
>>> try:
...     raise namespace["TestError"]("What's going on?")
... except Exception as e:
...     tb = traceback.TracebackException.from_exception(e)
...     print(*tb.format())
...
Traceback (most recent call last):
   File "<stdin>", line 2, in <module>
 some.module.TestError: What's going on?

or in the context of your code:

def exception_injector(name, parent, module_dict):
    class product_exception(parent, details=args):
        pass

    product_exception.__name__ = name
    product_exception.__qualname__ = name
    product_exception.__module__ = module_dict["__name__"]
    module_dict[name] = product_exception

* The traceback module omits the exception module if the module name is either __main__ or builtins.

Related