plugin manager can accumulate stale entries?
I tried uninstalling and reinstalling the volume plugin. While the plugin was uninstalled, I still saw the vispy volume renderer as an option in the "manage plugins" window. How is that list computed? Does it remove entries appropriately?

diff --git a/.travis.yml b/.travis.yml
index 4a1fbd4b..1d56bbeb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -51,7 +51,7 @@ matrix:
             - ASTRO_VER=None
             - MPL_VER=1.4
             - PIP_BASE="pytest-cov coveralls"
-            - QT_PKG=pyside
+            - QT_PKG=pyqt
 
         - os: linux
           env:
@@ -71,10 +71,10 @@ matrix:
           env: PYTHON_VERSION=2.7 MPL_VER=1.3 ASTRO_VER=0.3 NUMPY_VER=1.8 QT_PKG=pyqt
 
         - os: linux
-          env: PYTHON_VERSION=2.7 MPL_VER=1.4 ASTRO_VER=0.4 IPYTHON_VER=1.1 QT_PKG=pyside
+          env: PYTHON_VERSION=2.7 MPL_VER=1.4 ASTRO_VER=0.4 IPYTHON_VER=1.1 QT_PKG=pyqt
 
         - os: linux
-          env: PYTHON_VERSION=2.7 MPL_VER=1.4 ASTRO_VER=0.4 IPYTHON_VER=0.13 QT_PKG=pyside
+          env: PYTHON_VERSION=2.7 MPL_VER=1.4 ASTRO_VER=0.4 IPYTHON_VER=0.13 QT_PKG=pyqt
 
 
 before_install:
@@ -123,10 +123,13 @@ install:
   # number.
   - if [ $DOC_TRIGGER ]; then pip install astropy-helpers linkchecker; fi
 
+  # Uninstal PyQt if we are using PySide
+  - if [ $QT_PKG == pyside ]; then conda remove --yes pyqt sip; fi
+
   - python setup.py install
 
 script:
-  - py.test $PYTEST_ARGS glue
+  - py.test $PYTEST_ARGS glue -s
   - if [ $DOC_TRIGGER ]; then cd doc && make html 2> warnings.log && cd ../ ; fi
 
   # In the following command, the test -s ensures that warnings are not emitted
diff --git a/glue/_plugin_helpers.py b/glue/_plugin_helpers.py
index a6a402c3..e2825abc 100644
--- a/glue/_plugin_helpers.py
+++ b/glue/_plugin_helpers.py
@@ -33,7 +33,7 @@ class PluginConfig(object):
         # just import the variable directly otherwise it is cached.
         from . import config
         cfg_dir = config.CFG_DIR
-        
+
         plugin_cfg =  os.path.join(cfg_dir, 'plugins.cfg')
 
         from .external.six.moves import configparser
@@ -74,3 +74,13 @@ class PluginConfig(object):
 
         with open(plugin_cfg, 'w') as fout:
             config.write(fout)
+
+    def filter(self, keep):
+        """
+        Keep only certain plugins.
+
+        This is used to filter out plugins that are not installed.
+        """
+        for key in list(self.plugins.keys())[:]:
+            if not key in keep:
+                self.plugins.pop(key)
diff --git a/glue/clients/tests/test_histogram_client.py b/glue/clients/tests/test_histogram_client.py
index 614f1fa4..0ffc7875 100644
--- a/glue/clients/tests/test_histogram_client.py
+++ b/glue/clients/tests/test_histogram_client.py
@@ -20,10 +20,6 @@ from .util import renderless_figure
 FIGURE = renderless_figure()
 
 
-class TestException(Exception):
-    pass
-
-
 class TestHistogramClient(object):
 
     def setup_method(self, method):
diff --git a/glue/core/data_factories/tables.py b/glue/core/data_factories/tables.py
index 181a81c4..d941f17a 100644
--- a/glue/core/data_factories/tables.py
+++ b/glue/core/data_factories/tables.py
@@ -10,7 +10,7 @@ from .astropy_table import astropy_tabular_data
 __all__ = ['tabular_data']
 
 
-@data_factory(label="Catalog",
+@data_factory(label="ASCII Table",
               identifier=has_extension('csv txt tsv tbl dat '
                                        'csv.gz txt.gz tbl.bz '
                                        'dat.gz'),
diff --git a/glue/core/tests/test_communication.py b/glue/core/tests/test_communication.py
index 3fbe45f4..14c18fb6 100644
--- a/glue/core/tests/test_communication.py
+++ b/glue/core/tests/test_communication.py
@@ -26,7 +26,7 @@ Processed (or ignored!) by clients
 """
 
 
-class TestClient(Client):
+class _TestClient(Client):
 
     def __init__(self, data):
         Client.__init__(self, data)
@@ -58,9 +58,9 @@ class TestCommunication(object):
         self.d2 = Data()
         self.d3 = Data()
         dc = DataCollection([self.d1])
-        self.c1 = TestClient(dc)
-        self.c2 = TestClient(DataCollection([self.d2]))
-        self.c3 = TestClient(dc)
+        self.c1 = _TestClient(dc)
+        self.c2 = _TestClient(DataCollection([self.d2]))
+        self.c3 = _TestClient(dc)
         self.s1 = Subset(self.d1)
         self.s2 = Subset(self.d2)
         self.m1 = SubsetCreateMessage(self.s1)
@@ -74,7 +74,7 @@ class TestCommunication(object):
 
         h = Hub()
         d = Data()
-        c = TestClient(DataCollection([d]))
+        c = _TestClient(DataCollection([d]))
         assert not c in h._subscriptions
         c.register_to_hub(h)
         assert c in h._subscriptions
@@ -118,7 +118,7 @@ class TestCommunication(object):
         assert self.c1.last_message is None
         assert self.c1.call is None
 
-    @pytest.mark.skip("Relaxed requirement. Hub now ignores exceptions")
+    @pytest.mark.skipif(True, reason="Relaxed requirement. Hub now ignores exceptions")
     def test_uncaught_message(self):
         #broadcast a message without a message handler
         self.hub.subscribe(self.c1, Message)
@@ -175,7 +175,7 @@ class TestCommunication(object):
         #sends messages
         d = Data()
         dc = DataCollection(d)
-        c = TestClient(dc)
+        c = _TestClient(dc)
 
         c.register_to_hub(dc.hub)
         sub = d.new_subset()
diff --git a/glue/core/tests/test_data.py b/glue/core/tests/test_data.py
index a43da69e..255dc699 100644
--- a/glue/core/tests/test_data.py
+++ b/glue/core/tests/test_data.py
@@ -18,7 +18,7 @@ from ... import core
 from ...external import six
 
 
-class TestCoordinates(Coordinates):
+class _TestCoordinates(Coordinates):
 
     def pixel2world(self, *args):
         return [(i + 2.) * a for i, a in enumerate(args)]
@@ -34,7 +34,7 @@ class TestData(object):
         Registry().clear()
         comp = Component(np.random.random((2, 3)))
         self.comp = comp
-        self.data.coords = TestCoordinates()
+        self.data.coords = _TestCoordinates()
         self.comp_id = self.data.add_component(comp, 'Test Component')
 
     def test_2d_component_print(self):
diff --git a/glue/main.py b/glue/main.py
index 5e700f65..ec7602ce 100755
--- a/glue/main.py
+++ b/glue/main.py
@@ -242,6 +242,7 @@ def main(argv=sys.argv):
 
 
 _loaded_plugins = set()
+_installed_plugins = set()
 
 
 def load_plugins():
@@ -268,6 +269,9 @@ def load_plugins():
 
     for item in iter_plugin_entry_points():
 
+        if not item.module_name in _installed_plugins:
+            _installed_plugins.add(item.name)
+
         if item.module_name in _loaded_plugins:
             logger.info("Plugin {0} already loaded".format(item.name))
             continue
diff --git a/glue/qt/glue_application.py b/glue/qt/glue_application.py
index 2ada01a2..a291c2d5 100644
--- a/glue/qt/glue_application.py
+++ b/glue/qt/glue_application.py
@@ -913,8 +913,9 @@ class GlueApplication(Application, QMainWindow):
         qmb.exec_()
 
     def plugin_manager(self):
-        pm = QtPluginManager()
-        pm.exec_()
+        from ..main import _installed_plugins
+        pm = QtPluginManager(installed=_installed_plugins)
+        pm.ui.exec_()
 
     def _update_undo_redo_enabled(self):
         undo, redo = self._cmds.can_undo_redo()
diff --git a/glue/qt/plugin_manager.py b/glue/qt/plugin_manager.py
index a5672e6f..13a1005c 100644
--- a/glue/qt/plugin_manager.py
+++ b/glue/qt/plugin_manager.py
@@ -8,33 +8,33 @@ from .qtutil import load_ui
 __all__ = ["QtPluginManager"]
 
 
-class QtPluginManager(QtGui.QDialog):
+class QtPluginManager(object):
 
-    def __init__(self):
+    def __init__(self, installed=None):
 
-        super(QtPluginManager, self).__init__()
+        self.ui = load_ui('plugin_manager.ui', None)
 
-        self.ui = load_ui('plugin_manager.ui', self)
-
-        self.cancel.clicked.connect(self.reject)
-        self.confirm.clicked.connect(self.finalize)
+        self.ui.cancel.clicked.connect(self.reject)
+        self.ui.confirm.clicked.connect(self.finalize)
 
         self._checkboxes = {}
 
-        self.update_list()
+        self.update_list(installed=installed)
 
     def clear(self):
         self._checkboxes.clear()
-        self.tree.clear()
+        self.ui.tree.clear()
 
-    def update_list(self):
+    def update_list(self, installed=None):
 
         self.clear()
 
         config = PluginConfig.load()
+        if installed is not None:
+            config.filter(installed)
 
         for plugin in sorted(config.plugins):
-            check = QtGui.QTreeWidgetItem(self.tree.invisibleRootItem(),
+            check = QtGui.QTreeWidgetItem(self.ui.tree.invisibleRootItem(),
                                           ["", plugin])
             check.setFlags(check.flags() | QtCore.Qt.ItemIsUserCheckable)
             if config.plugins[plugin]:
@@ -43,8 +43,11 @@ class QtPluginManager(QtGui.QDialog):
                 check.setCheckState(0, QtCore.Qt.Unchecked)
             self._checkboxes[plugin] = check
 
-        self.tree.resizeColumnToContents(0)
-        self.tree.resizeColumnToContents(1)
+        self.ui.tree.resizeColumnToContents(0)
+        self.ui.tree.resizeColumnToContents(1)
+
+    def reject(self):
+        self.ui.reject()
 
     def finalize(self):
 
@@ -66,4 +69,4 @@ class QtPluginManager(QtGui.QDialog):
             message.exec_()
             return
 
-        self.accept()
+        self.ui.accept()
diff --git a/glue/qt/tests/test_application.py b/glue/qt/tests/test_application.py
index 58f76167..cd80934c 100644
--- a/glue/qt/tests/test_application.py
+++ b/glue/qt/tests/test_application.py
@@ -27,6 +27,7 @@ from ...external.six import PY3
 
 from ...tests.helpers import requires_ipython_ge_012
 
+os.environ['GLUE_TESTING'] = 'True'
 
 def tab_count(app):
     return app.tab_bar.count()
diff --git a/glue/qt/tests/test_toolbar.py b/glue/qt/tests/test_toolbar.py
index b560dbc6..a9356b26 100644
--- a/glue/qt/tests/test_toolbar.py
+++ b/glue/qt/tests/test_toolbar.py
@@ -10,10 +10,10 @@ from ..mouse_mode import MouseMode
 from ..qtutil import get_icon
 
 
-class TestMode(MouseMode):
+class MouseModeTest(MouseMode):
 
     def __init__(self, axes, release_callback=None):
-        super(TestMode, self).__init__(axes, release_callback=release_callback)
+        super(MouseModeTest, self).__init__(axes, release_callback=release_callback)
         self.icon = get_icon('square')
         self.mode_id = 'TEST'
         self.action_text = 'test text'
@@ -37,11 +37,14 @@ class TestToolbar(object):
         self.canvas = widget.canvas
         self.axes = axes
         self.tb = GlueToolbar(self.canvas, self.win)
-        self.mode = TestMode(self.axes, release_callback=self.callback)
+        self.mode = MouseModeTest(self.axes, release_callback=self.callback)
         self.tb.add_mode(self.mode)
         self.win.addToolBar(self.tb)
         self._called_back = False
 
+    def teardown_method(self, method):
+        self.win.close()
+
     def _make_plot_widget(self, parent):
         widget = MplWidget(parent)
         ax = widget.canvas.fig.add_subplot(111)
diff --git a/glue/qt/widgets/layer_tree_widget.py b/glue/qt/widgets/layer_tree_widget.py
index 35a9982c..9ece584b 100644
--- a/glue/qt/widgets/layer_tree_widget.py
+++ b/glue/qt/widgets/layer_tree_widget.py
@@ -441,7 +441,7 @@ class LayerTreeWidget(QWidget, Ui_LayerTree):
     def _connect(self):
         """ Connect widget signals to methods """
         self._actions['link'] = LinkAction(self)
-        self.layerAddButton.clicked.connect(self._load_data)
+        self.layerAddButton.clicked.connect(nonpartial(self._load_data))
         self.layerRemoveButton.clicked.connect(self._actions['delete'].trigger)
         self.linkButton.set_action(self._actions['link'])
         self.newSubsetButton.set_action(self._actions['new'], text=False)
diff --git a/glue/qt/widgets/tests/test_image_widget.py b/glue/qt/widgets/tests/test_image_widget.py
index 86c8f14d..1cb885b0 100644
--- a/glue/qt/widgets/tests/test_image_widget.py
+++ b/glue/qt/widgets/tests/test_image_widget.py
@@ -350,3 +350,6 @@ def test_combo_box_updates():
     assert exc.value.args[0] == "Cannot find data 'a' in combo box"
 
     assert widget.client.display_data is data1
+
+
+del TestApplication
\ No newline at end of file
diff --git a/glue/qt/widgets/tests/test_settings.py b/glue/qt/widgets/tests/test_settings.py
index e54dd3a6..d2eaf609 100644
--- a/glue/qt/widgets/tests/test_settings.py
+++ b/glue/qt/widgets/tests/test_settings.py
@@ -24,6 +24,9 @@ class TestSettings(object):
         self.editor = SettingsEditor(self.a)
         self.widget = self.editor.widget
 
+    def teardown_method(self, method):
+        self.widget.close()
+
     def test_init(self):
         assert self.widget.item(0, 0).text() == 'k1'
         assert self.widget.item(0, 1).text() == 'v1'
['glue/qt/tests/test_toolbar.py', 'glue/qt/widgets/tests/test_image_widget.py', 'glue/qt/tests/test_application.py', 'glue/qt/plugin_manager.py', 'glue/qt/widgets/tests/test_settings.py', 'glue/_plugin_helpers.py', 'glue/core/tests/test_data.py', 'glue/main.py', 'glue/clients/tests/test_histogram_client.py', 'glue/core/data_factories/tables.py', 'glue/qt/glue_application.py', '.travis.yml', 'glue/qt/widgets/layer_tree_widget.py', 'glue/core/tests/test_communication.py']