Repository: enthought/traits-futures

Issue:
stop method creates message router if it doesn't already exist
The message router for the `TraitsExecutor` is created on first use. If no jobs are submitted, that first use is at executor `stop` time: the message router is instantiated only to call its `disconnect` method. It would be better to rearrange the logic so that the message router is either always created at `__init__` time, or not created at all if it's not used.

Discussion:
mdickinson: Self-merging after self-review

Patch:
diff --git a/traits_futures/traits_executor.py b/traits_futures/traits_executor.py
index 4ca4183..43a6755 100644
--- a/traits_futures/traits_executor.py
+++ b/traits_futures/traits_executor.py
@@ -289,6 +289,9 @@ class TraitsExecutor(HasStrictTraits):
     #: and foreground futures.
     _message_router = Any()
 
+    #: True if we've created a message router, and need to shut it down.
+    _have_message_router = Bool(False)
+
     #: Wrappers for currently-executing futures.
     _wrappers = Dict(Any(), Any())
 
@@ -315,6 +318,7 @@ class TraitsExecutor(HasStrictTraits):
         # Toolkit-specific message router.
         router = self._context.message_router()
         router.connect()
+        self._have_message_router = True
         return router
 
     def __context_default(self):
@@ -338,8 +342,10 @@ class TraitsExecutor(HasStrictTraits):
         Go to STOPPED state, and shut down the worker pool if we own it.
         """
         assert self.state == STOPPING
-        self._message_router.disconnect()
-        self._message_router = None
+
+        if self._have_message_router:
+            self._message_router.disconnect()
+            self._message_router = None
 
         if self._own_worker_pool:
             self._worker_pool.shutdown()


Test patch:
diff --git a/traits_futures/tests/test_traits_executor.py b/traits_futures/tests/test_traits_executor.py
index 0bced3d..d7f5d35 100644
--- a/traits_futures/tests/test_traits_executor.py
+++ b/traits_futures/tests/test_traits_executor.py
@@ -7,6 +7,8 @@ Tests for the TraitsExecutor class.
 import contextlib
 import unittest
 
+from traits.api import Bool
+
 from traits_futures.api import MultithreadingContext, TraitsExecutor
 from traits_futures.tests.traits_executor_tests import (
     ExecutorListener,
@@ -17,6 +19,27 @@ from traits_futures.toolkit_support import toolkit
 GuiTestAssistant = toolkit("gui_test_assistant:GuiTestAssistant")
 
 
+class TrackingTraitsExecutor(TraitsExecutor):
+    """
+    Version of TraitsExecutor that keeps track of whether the default
+    methods have been called, for testing purposes.
+    """
+
+    #: Have we created a message router?
+    _message_router_created = Bool(False)
+
+    def __message_router_default(self):
+        self._message_router_created = True
+        return TraitsExecutor._TraitsExecutor__message_router_default(self)
+
+    #: Have we created a context?
+    _context_created = Bool(False)
+
+    def __context_default(self):
+        self._context_created = True
+        return TraitsExecutor._TraitsExecutor__context_default(self)
+
+
 class TestTraitsExecutorCreation(GuiTestAssistant, unittest.TestCase):
     def setUp(self):
         GuiTestAssistant.setUp(self)
@@ -100,6 +123,22 @@ class TestTraitsExecutorCreation(GuiTestAssistant, unittest.TestCase):
             cf_future = worker_pool.submit(int)
             self.assertEqual(cf_future.result(), 0)
 
+    def test_no_objects_created_at_shutdown(self):
+        # An executor that has no jobs submitted to it should not
+        # need to instantiate either the context or the message router.
+        with self.temporary_worker_pool() as worker_pool:
+            executor = TrackingTraitsExecutor(worker_pool=worker_pool)
+            executor.stop()
+            self.wait_until_stopped(executor)
+
+        self.assertFalse(
+            executor._message_router_created,
+            msg="Message router unexpectedly created",
+        )
+        self.assertFalse(
+            executor._context_created, msg="Context unexpectedly created",
+        )
+
     def wait_until_stopped(self, executor):
         """"
         Wait for the executor to reach STOPPED state.