Repository: enthought/traits-futures

Issue:
Consider using `type.__name__` instead of `str(type)` in `marshal_exception()`
Consider using `type.__name__` instead of `str(type)` in `marshal_exception()` for better use of that string.

In my opinion, `'ZeroDivisionError'` is more useful than `"<class 'ZeroDivisionError'>"`.

https://github.com/enthought/traits-futures/blob/main/traits_futures/exception_handling.py#L22

https://github.com/enthought/traits-futures/blob/839a775285ee0b28be81672734b0f196bfb596c5/traits_futures/exception_handling.py#L17-L25

Discussion:

Patch:
diff --git a/traits_futures/exception_handling.py b/traits_futures/exception_handling.py
index c382978..1165b38 100644
--- a/traits_futures/exception_handling.py
+++ b/traits_futures/exception_handling.py
@@ -14,6 +14,34 @@ Support for transferring exception information from a background task.
 import traceback
 
 
+def _qualified_type_name(class_):
+    """
+    Compute a descriptive string representing a class, including
+    a module name where relevant.
+
+    Example outputs are "RuntimeError" for the built-in RuntimeError
+    exception, or "struct.error" for the struct module exception class.
+
+    Parameters
+    ----------
+    class_ : type
+
+    Returns
+    -------
+    class_name : str
+    """
+    # We're being extra conservative here and allowing for the possibility that
+    # the class doesn't have __module__ and/or __qualname__ attributes. This
+    # function is called during exception handling, so we want to minimise the
+    # possibility that it raises a new exception.
+    class_module = getattr(class_, "__module__", "<unknown>")
+    class_qualname = getattr(class_, "__qualname__", "<unknown>")
+    if class_module == "builtins":
+        return f"{class_qualname}"
+    else:
+        return f"{class_module}.{class_qualname}"
+
+
 def marshal_exception(exception):
     """
     Turn exception details into something that can be safely
@@ -31,7 +59,7 @@ def marshal_exception(exception):
         formatted traceback.
     """
     return (
-        str(type(exception)),
+        _qualified_type_name(type(exception)),
         str(exception),
         "".join(
             traceback.format_exception(


Test patch:
diff --git a/traits_futures/tests/background_call_tests.py b/traits_futures/tests/background_call_tests.py
index 62661ed..d79c0e3 100644
--- a/traits_futures/tests/background_call_tests.py
+++ b/traits_futures/tests/background_call_tests.py
@@ -292,7 +292,7 @@ class BackgroundCallTests:
             future.result
 
     def assertException(self, future, exc_type):
-        self.assertEqual(future.exception[0], str(exc_type))
+        self.assertIn(exc_type.__name__, future.exception[0])
 
     def assertNoException(self, future):
         with self.assertRaises(AttributeError):
diff --git a/traits_futures/tests/background_iteration_tests.py b/traits_futures/tests/background_iteration_tests.py
index 754740f..bc78e8e 100644
--- a/traits_futures/tests/background_iteration_tests.py
+++ b/traits_futures/tests/background_iteration_tests.py
@@ -448,7 +448,7 @@ class BackgroundIterationTests:
             future.result
 
     def assertException(self, future, exc_type):
-        self.assertEqual(future.exception[0], str(exc_type))
+        self.assertIn(exc_type.__name__, future.exception[0])
 
     def assertNoException(self, future):
         with self.assertRaises(AttributeError):
diff --git a/traits_futures/tests/background_progress_tests.py b/traits_futures/tests/background_progress_tests.py
index 9f8c5a6..9d11bd6 100644
--- a/traits_futures/tests/background_progress_tests.py
+++ b/traits_futures/tests/background_progress_tests.py
@@ -332,4 +332,4 @@ class BackgroundProgressTests:
             future.exception
 
     def assertException(self, future, exc_type):
-        self.assertEqual(future.exception[0], str(exc_type))
+        self.assertIn(exc_type.__name__, future.exception[0])
diff --git a/traits_futures/tests/test_exception_handling.py b/traits_futures/tests/test_exception_handling.py
index 6de33ed..894e4c1 100644
--- a/traits_futures/tests/test_exception_handling.py
+++ b/traits_futures/tests/test_exception_handling.py
@@ -13,6 +13,10 @@ import unittest
 from traits_futures.exception_handling import marshal_exception
 
 
+class CustomException(Exception):
+    """Custom exception for testing purposes."""
+
+
 class TestExceptionHandling(unittest.TestCase):
     def test_marshal_exception(self):
         try:
@@ -25,7 +29,7 @@ class TestExceptionHandling(unittest.TestCase):
         self.assertIsInstance(exc_value, str)
         self.assertIsInstance(exc_traceback, str)
 
-        self.assertEqual(exc_type, str(RuntimeError))
+        self.assertEqual(exc_type, "RuntimeError")
         self.assertIn("something went wrong", exc_value)
         self.assertIn("test_marshal_exception", exc_traceback)
 
@@ -41,7 +45,7 @@ class TestExceptionHandling(unittest.TestCase):
         self.assertIsInstance(exc_value, str)
         self.assertIsInstance(exc_traceback, str)
 
-        self.assertEqual(exc_type, str(ValueError))
+        self.assertEqual(exc_type, "ValueError")
         self.assertIn(message, exc_value)
         self.assertIn("test_marshal_exception", exc_traceback)
 
@@ -59,6 +63,41 @@ class TestExceptionHandling(unittest.TestCase):
         self.assertIsInstance(exc_value, str)
         self.assertIsInstance(exc_traceback, str)
 
-        self.assertEqual(exc_type, str(RuntimeError))
+        self.assertEqual(exc_type, "RuntimeError")
         self.assertIn("something went wrong", exc_value)
         self.assertIn("test_marshal_exception", exc_traceback)
+
+    def test_marshal_exception_non_builtin(self):
+        message = "printer on fire"
+        try:
+            raise CustomException(message)
+        except BaseException as exception:
+            marshalled = marshal_exception(exception)
+
+        exc_type, exc_value, exc_traceback = marshalled
+        self.assertIsInstance(exc_type, str)
+        self.assertIsInstance(exc_value, str)
+        self.assertIsInstance(exc_traceback, str)
+
+        self.assertEqual(
+            exc_type,
+            f"{__name__}.CustomException",
+        )
+        self.assertIn(message, exc_value)
+        self.assertIn("test_marshal_exception", exc_traceback)
+
+    def test_marshal_exception_nested_exception(self):
+        class NestedException(Exception):
+            pass
+
+        try:
+            raise NestedException()
+        except BaseException as exception:
+            marshalled = marshal_exception(exception)
+
+        exc_type, exc_value, exc_traceback = marshalled
+        self.assertEqual(
+            exc_type,
+            f"{__name__}.TestExceptionHandling."
+            "test_marshal_exception_nested_exception.<locals>.NestedException",
+        )