From 81596d598d23042a5ee4d270e185aaf5c0175967 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 13 Oct 2017 17:14:54 +0100 Subject: [PATCH 01/20] Fix updating of layer icons when layer states are changed - now any change in layer state will force an update of the layer list viewport. Also make sure that for scatter plots with linear colors, the icon shows the colormap. --- CHANGES.md | 3 +++ glue/core/message.py | 6 ++++++ glue/core/qt/layer_artist_model.py | 4 ++-- glue/viewers/matplotlib/state.py | 7 +++++++ glue/viewers/scatter/layer_artist.py | 6 ++++++ glue/viewers/scatter/state.py | 4 +--- 6 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 31b6b6843..dd75d3ec3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,9 @@ v0.11.2 (unreleased) * Fixed bug in spectrum tool that caused the upper range in aggregations to be incorrectly calculated. [#1402] +* Fixed icon for scatter plot layer when a colormap is used, and fix issues with + viewer layer icons not updating immediately. + v0.11.1 (2017-08-25) -------------------- diff --git a/glue/core/message.py b/glue/core/message.py index 272ab0d46..856c3a00c 100644 --- a/glue/core/message.py +++ b/glue/core/message.py @@ -213,6 +213,12 @@ def __init__(self, sender, tag=None): self.layer_artist = self.sender +class LayerArtistUpdatedMessage(Message): + def __init__(self, sender, tag=None): + super(LayerArtistUpdatedMessage, self).__init__(sender, tag=tag) + self.layer_artist = self.sender + + class LayerArtistDisabledMessage(Message): def __init__(self, sender, tag=None): super(LayerArtistDisabledMessage, self).__init__(sender, tag=tag) diff --git a/glue/core/qt/layer_artist_model.py b/glue/core/qt/layer_artist_model.py index 66a61f70d..d8cd9bb4d 100644 --- a/glue/core/qt/layer_artist_model.py +++ b/glue/core/qt/layer_artist_model.py @@ -22,7 +22,7 @@ from glue.utils import nonpartial from glue.utils.qt import PythonListModel, PyMimeData from glue.core.hub import HubListener -from glue.core.message import Message, LayerArtistEnabledMessage, LayerArtistDisabledMessage +from glue.core.message import Message, LayerArtistEnabledMessage, LayerArtistUpdatedMessage, LayerArtistDisabledMessage class LayerArtistModel(PythonListModel): @@ -203,11 +203,11 @@ def __init__(self, parent=None, hub=None): # listen to all events since the viewport update is fast. self.hub = hub self.hub.subscribe(self, Message, self._update_viewport) + self.hub.subscribe(self, LayerArtistUpdatedMessage, self._layer_enabled_or_disabled) self.hub.subscribe(self, LayerArtistEnabledMessage, self._layer_enabled_or_disabled) self.hub.subscribe(self, LayerArtistDisabledMessage, self._layer_enabled_or_disabled) def _update_viewport(self, *args): - # This forces the widget containing the list view to update/redraw, # reflecting any changes in color/labels/content self.viewport().update() diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py index 6157d1a1b..d31e6b536 100644 --- a/glue/viewers/matplotlib/state.py +++ b/glue/viewers/matplotlib/state.py @@ -4,6 +4,7 @@ SelectionCallbackProperty, keep_in_sync) from glue.core.state_objects import State +from glue.core.message import LayerArtistUpdatedMessage from glue.utils import defer_draw @@ -89,3 +90,9 @@ def __init__(self, viewer_state=None, **kwargs): self._sync_color = keep_in_sync(self, 'color', self.layer.style, 'color') self._sync_alpha = keep_in_sync(self, 'alpha', self.layer.style, 'alpha') + + self.add_global_callback(self._notify_layer_update) + + def _notify_layer_update(self, **kwargs): + message = LayerArtistUpdatedMessage(self) + self.layer.hub.broadcast(message) diff --git a/glue/viewers/scatter/layer_artist.py b/glue/viewers/scatter/layer_artist.py index 626cf5129..dd7d16d59 100644 --- a/glue/viewers/scatter/layer_artist.py +++ b/glue/viewers/scatter/layer_artist.py @@ -297,6 +297,12 @@ def _update_scatter(self, force=False, **kwargs): if force or len(changed & VISUAL_PROPERTIES) > 0: self._update_visual_attributes(changed, force=force) + def get_layer_color(self): + if self.state.style != 'Scatter' or self.state.cmap_mode == 'Fixed': + return self.state.color + else: + return self.state.cmap + @defer_draw def update(self): diff --git a/glue/viewers/scatter/state.py b/glue/viewers/scatter/state.py index 8328d41e0..4dce28b69 100644 --- a/glue/viewers/scatter/state.py +++ b/glue/viewers/scatter/state.py @@ -2,9 +2,7 @@ from __future__ import absolute_import, division, print_function -import numpy as np - -from glue.core import Data, Subset +from glue.core import Data from glue.config import colormaps from glue.viewers.matplotlib.state import (MatplotlibDataViewerState, From 1468af86baad5cb14a3a3927be355240379bc694 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 13 Oct 2017 17:30:04 +0100 Subject: [PATCH 02/20] Make drag and dropping of session files onto glue work properly, and warn about loss of session when opening a session file. --- CHANGES.md | 4 ++++ glue/app/qt/application.py | 28 ++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dd75d3ec3..9bde78581 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,10 @@ v0.11.2 (unreleased) * Fixed icon for scatter plot layer when a colormap is used, and fix issues with viewer layer icons not updating immediately. +* Fixed dragging and dropping session files onto glue (this now loads the session + rather than trying to load it as a dataset). Also now show a warning when + the application is about to be reset to open a new session. + v0.11.1 (2017-08-25) -------------------- diff --git a/glue/app/qt/application.py b/glue/app/qt/application.py index 49c570e2d..d6c07a7eb 100644 --- a/glue/app/qt/application.py +++ b/glue/app/qt/application.py @@ -868,9 +868,7 @@ def _restore_session(self, show=True): if not file_name: return - with set_cursor_cm(Qt.WaitCursor): - ga = self.restore_session(file_name) - self.close() + ga = self.restore_session_and_close(file_name) return ga def _reset_session(self, show=True, warn=True): @@ -1032,9 +1030,31 @@ def dropEvent(self, event): if path.startswith('/') and path[2] == ':': path = path[1:] - self.load_data(path) + if path.endswith('.glu'): + self.restore_session_and_close(path) + else: + self.load_data(path) + event.accept() + def restore_session_and_close(self, path): + + buttons = QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel + + dialog = QtWidgets.QMessageBox.warning( + self, "Confirm Close", + "Loading a session file will close the existing session. Are you " + "sure you want to continue?", + buttons=buttons, defaultButton=QtWidgets.QMessageBox.Cancel) + + if not dialog == QtWidgets.QMessageBox.Ok: + return + + with set_cursor_cm(Qt.WaitCursor): + app = self.restore_session(path) + app.setGeometry(self.geometry()) + self.close() + def closeEvent(self, event): """Emit a message to hub before closing.""" for tab in self.viewers: From 567a631b0cfb5789b85da8df051970271bc8589e Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 13 Oct 2017 17:40:40 +0100 Subject: [PATCH 03/20] Make sure no errors happen if making a selection in an empty viewer --- CHANGES.md | 2 ++ glue/viewers/histogram/qt/data_viewer.py | 10 +++++++--- glue/viewers/histogram/qt/tests/test_data_viewer.py | 6 ++++++ glue/viewers/histogram/state.py | 4 ++++ glue/viewers/image/qt/data_viewer.py | 10 +++++++--- glue/viewers/image/qt/tests/test_data_viewer.py | 6 ++++++ glue/viewers/scatter/qt/data_viewer.py | 10 +++++++--- glue/viewers/scatter/qt/tests/test_data_viewer.py | 6 ++++++ 8 files changed, 45 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9bde78581..78bcf2004 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,8 @@ v0.11.2 (unreleased) rather than trying to load it as a dataset). Also now show a warning when the application is about to be reset to open a new session. +* Make sure no errors happen if making a selection in an empty viewer. + v0.11.1 (2017-08-25) -------------------- diff --git a/glue/viewers/histogram/qt/data_viewer.py b/glue/viewers/histogram/qt/data_viewer.py index 1fe668011..5f2302f53 100644 --- a/glue/viewers/histogram/qt/data_viewer.py +++ b/glue/viewers/histogram/qt/data_viewer.py @@ -57,9 +57,13 @@ def _update_axes(self): # TODO: move some of the ROI stuff to state class? def apply_roi(self, roi): - cmd = command.ApplyROI(data_collection=self._data, - roi=roi, apply_func=self._apply_roi) - self._session.command_stack.do(cmd) + if len(self.layers) > 0: + cmd = command.ApplyROI(data_collection=self._data, + roi=roi, apply_func=self._apply_roi) + self._session.command_stack.do(cmd) + else: + # Make sure we force a redraw to get rid of the ROI + self.axes.figure.canvas.draw() def _apply_roi(self, roi): diff --git a/glue/viewers/histogram/qt/tests/test_data_viewer.py b/glue/viewers/histogram/qt/tests/test_data_viewer.py index 3771c4a59..80583421c 100644 --- a/glue/viewers/histogram/qt/tests/test_data_viewer.py +++ b/glue/viewers/histogram/qt/tests/test_data_viewer.py @@ -293,6 +293,12 @@ def test_apply_roi_categorical(self): assert_equal(state.roi.categories, ['a', 'b']) + def test_apply_roi_empty(self): + # Make sure that doing an ROI selection on an empty viewer doesn't + # produce error messsages + roi = XRangeROI(-0.2, 0.1) + self.viewer.apply_roi(roi) + def test_axes_labels(self): viewer_state = self.viewer.state diff --git a/glue/viewers/histogram/state.py b/glue/viewers/histogram/state.py index 3d2465db1..9b31ad5af 100644 --- a/glue/viewers/histogram/state.py +++ b/glue/viewers/histogram/state.py @@ -112,6 +112,10 @@ def bins(self): """ The position of the bins for the histogram based on the current state. """ + + if self.hist_x_min is None or self.hist_x_max is None or self.hist_n_bin is None: + return None + if self.x_log: return np.logspace(np.log10(self.hist_x_min), np.log10(self.hist_x_max), diff --git a/glue/viewers/image/qt/data_viewer.py b/glue/viewers/image/qt/data_viewer.py index 73771d8fe..e9024fdb1 100644 --- a/glue/viewers/image/qt/data_viewer.py +++ b/glue/viewers/image/qt/data_viewer.py @@ -142,9 +142,13 @@ def _set_wcs(self, event=None, relim=True): # TODO: move some of the ROI stuff to state class? def apply_roi(self, roi): - cmd = command.ApplyROI(data_collection=self._data, - roi=roi, apply_func=self._apply_roi) - self._session.command_stack.do(cmd) + if len(self.layers) > 0: + cmd = command.ApplyROI(data_collection=self._data, + roi=roi, apply_func=self._apply_roi) + self._session.command_stack.do(cmd) + else: + # Make sure we force a redraw to get rid of the ROI + self.axes.figure.canvas.draw() def _apply_roi(self, roi): diff --git a/glue/viewers/image/qt/tests/test_data_viewer.py b/glue/viewers/image/qt/tests/test_data_viewer.py index 98ccb0f75..e1030c0e0 100644 --- a/glue/viewers/image/qt/tests/test_data_viewer.py +++ b/glue/viewers/image/qt/tests/test_data_viewer.py @@ -182,6 +182,12 @@ def test_apply_roi(self): state = self.image1.subsets[0].subset_state assert isinstance(state, RoiSubsetState) + def test_apply_roi_empty(self): + # Make sure that doing an ROI selection on an empty viewer doesn't + # produce error messsages + roi = XRangeROI(-0.2, 0.1) + self.viewer.apply_roi(roi) + def test_identical(self): # Check what happens if we set both attributes to the same coordinates diff --git a/glue/viewers/scatter/qt/data_viewer.py b/glue/viewers/scatter/qt/data_viewer.py index 043330c2e..90a43d717 100644 --- a/glue/viewers/scatter/qt/data_viewer.py +++ b/glue/viewers/scatter/qt/data_viewer.py @@ -64,9 +64,13 @@ def _update_axes(self): # TODO: move some of the ROI stuff to state class? def apply_roi(self, roi): - cmd = command.ApplyROI(data_collection=self._data, - roi=roi, apply_func=self._apply_roi) - self._session.command_stack.do(cmd) + if len(self.layers) > 0: + cmd = command.ApplyROI(data_collection=self._data, + roi=roi, apply_func=self._apply_roi) + self._session.command_stack.do(cmd) + else: + # Make sure we force a redraw to get rid of the ROI + self.axes.figure.canvas.draw() def _apply_roi(self, roi): diff --git a/glue/viewers/scatter/qt/tests/test_data_viewer.py b/glue/viewers/scatter/qt/tests/test_data_viewer.py index 436de65af..d3f772e3b 100644 --- a/glue/viewers/scatter/qt/tests/test_data_viewer.py +++ b/glue/viewers/scatter/qt/tests/test_data_viewer.py @@ -188,6 +188,12 @@ def test_apply_roi_categorical(self): state = self.data.subsets[0].subset_state assert isinstance(state, AndState) + def test_apply_roi_empty(self): + # Make sure that doing an ROI selection on an empty viewer doesn't + # produce error messsages + roi = XRangeROI(-0.2, 0.1) + self.viewer.apply_roi(roi) + def test_axes_labels(self): viewer_state = self.viewer.state From e2e6097ba33221554de41b77e4033a3fdbc4f635 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 13 Oct 2017 17:53:46 +0100 Subject: [PATCH 04/20] Fix creating faceted subsets on Python 3.x when no dataset is selected --- CHANGES.md | 2 ++ glue/app/qt/layer_tree_widget.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 78bcf2004..783210897 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,8 @@ v0.11.2 (unreleased) * Make sure no errors happen if making a selection in an empty viewer. +* Fix creating faceted subsets on Python 3.x when no dataset is selected. + v0.11.1 (2017-08-25) -------------------- diff --git a/glue/app/qt/layer_tree_widget.py b/glue/app/qt/layer_tree_widget.py index 759812071..b005e0430 100644 --- a/glue/app/qt/layer_tree_widget.py +++ b/glue/app/qt/layer_tree_widget.py @@ -133,7 +133,7 @@ def _do_action(self): layers = self.selected_layers() try: default = layers[0].data - except (AttributeError, TypeError): + except (AttributeError, TypeError, IndexError): default = None SubsetFacet.facet(self._layer_tree.data_collection, parent=self._layer_tree, default=default) From 402b5ba2c62f4ee06fe0aa63fcf85ed73002e7a6 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 13 Oct 2017 18:07:27 +0100 Subject: [PATCH 05/20] Deal gracefully with cases where layers in image viewer don't have a global_sync attribute, and update scatter layer if components are added (e.g. via linking) --- CHANGES.md | 2 ++ glue/viewers/common/qt/data_viewer_with_state.py | 12 +++++++++--- glue/viewers/image/state.py | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 783210897..f7920aaad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,8 @@ v0.11.2 (unreleased) * Fix creating faceted subsets on Python 3.x when no dataset is selected. +* Fix issues with overlaying a scatter layer on an image. + v0.11.1 (2017-08-25) -------------------- diff --git a/glue/viewers/common/qt/data_viewer_with_state.py b/glue/viewers/common/qt/data_viewer_with_state.py index 0dd7d029a..d32e9c24c 100644 --- a/glue/viewers/common/qt/data_viewer_with_state.py +++ b/glue/viewers/common/qt/data_viewer_with_state.py @@ -138,6 +138,12 @@ def remove_subset(self, subset): def _add_subset(self, message): self.add_subset(message.subset) + def _update_data(self, message): + if message.data in self._layer_artist_container: + for layer_artist in self._layer_artist_container[message.data]: + layer_artist.update() + self.redraw() + def _update_subset(self, message): if message.subset in self._layer_artist_container: for layer_artist in self._layer_artist_container[message.subset]: @@ -186,9 +192,9 @@ def register_to_hub(self, hub): hub.subscribe(self, msg.DataCollectionDeleteMessage, handler=self._remove_data) - # hub.subscribe(self, msg.ComponentsChangedMessage, - # handler=self._update_data, - # filter=has_data) + hub.subscribe(self, msg.ComponentsChangedMessage, + handler=self._update_data, + filter=self._has_data_or_subset) hub.subscribe(self, msg.SettingsChangeMessage, self._update_appearance_from_settings, diff --git a/glue/viewers/image/state.py b/glue/viewers/image/state.py index bdbbba41d..dac476619 100644 --- a/glue/viewers/image/state.py +++ b/glue/viewers/image/state.py @@ -124,7 +124,9 @@ def _update_syncing(self): for data, layer_states in layer_state_by_data.items(): if len(layer_states) > 1: for layer_state in layer_states: - if layer_state.global_sync: + # Scatter layers don't have global_sync so we need to be + # careful here and make sure we return a default value + if getattr(layer_state, 'global_sync', False): layer_state.global_sync = False def _update_combo_ref_data(self): From 684099d78e09952b169f82193f2914cee45cb92f Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 14 Oct 2017 09:10:31 +0100 Subject: [PATCH 06/20] Fix issue with categorical labels not appearing after loading a session, and also with labels for negative indices when zooming out on a categorical plot. --- CHANGES.md | 4 ++ glue/core/util.py | 15 +++++-- glue/viewers/scatter/qt/data_viewer.py | 1 + .../scatter/qt/tests/test_data_viewer.py | 43 +++++++++++++++++-- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f7920aaad..a3f2b5b1c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,10 @@ v0.11.2 (unreleased) * Fix issues with overlaying a scatter layer on an image. +* Fix issues with labels for categorical axes in the scatter and histogram + viewers, in particular when loading viewers with categorical axes from + session files. + v0.11.1 (2017-08-25) -------------------- diff --git a/glue/core/util.py b/glue/core/util.py index 3e2ede913..c6ec742da 100644 --- a/glue/core/util.py +++ b/glue/core/util.py @@ -320,11 +320,18 @@ def visible_limits(artists, axis): def tick_linker(all_categories, pos, *args): - try: - pos = np.round(pos) - return all_categories[int(pos)] - except IndexError: + + # We need to take care to ignore negative indices since these would actually + # 'work' 'when accessing all_categories, but we need to avoid that. + if pos < 0 or pos >= len(all_categories): return '' + else: + try: + pos = np.round(pos) + print(all_categories[int(pos)]) + return all_categories[int(pos)] + except IndexError: + return '' def update_ticks(axes, coord, components, is_log): diff --git a/glue/viewers/scatter/qt/data_viewer.py b/glue/viewers/scatter/qt/data_viewer.py index 90a43d717..73a641130 100644 --- a/glue/viewers/scatter/qt/data_viewer.py +++ b/glue/viewers/scatter/qt/data_viewer.py @@ -36,6 +36,7 @@ def __init__(self, session, parent=None, state=None): self.state.add_callback('y_att', nonpartial(self._update_axes)) self.state.add_callback('x_log', nonpartial(self._update_axes)) self.state.add_callback('y_log', nonpartial(self._update_axes)) + self._update_axes() def _update_axes(self): diff --git a/glue/viewers/scatter/qt/tests/test_data_viewer.py b/glue/viewers/scatter/qt/tests/test_data_viewer.py index d3f772e3b..e484be9ce 100644 --- a/glue/viewers/scatter/qt/tests/test_data_viewer.py +++ b/glue/viewers/scatter/qt/tests/test_data_viewer.py @@ -16,11 +16,11 @@ from glue.core.subset import RoiSubsetState, AndState from glue import core from glue.core.component_id import ComponentID -from glue.core.tests.util import simple_session from glue.utils.qt import combo_as_string from glue.viewers.matplotlib.qt.tests.test_data_viewer import BaseTestMatplotlibDataViewer from glue.core.state import GlueUnSerializer from glue.app.qt.layer_tree_widget import LayerTreeWidget +from glue.app.qt import GlueApplication from ..data_viewer import ScatterViewer @@ -42,14 +42,15 @@ def setup_method(self, method): self.data_2d = Data(label='d2', a=[[1, 2], [3, 4]], b=[[5, 6], [7, 8]], x=[[3, 5], [5.4, 1]], y=[[1.2, 4], [7, 8]]) - self.session = simple_session() + self.app = GlueApplication() + self.session = self.app.session self.hub = self.session.hub self.data_collection = self.session.data_collection self.data_collection.append(self.data) self.data_collection.append(self.data_2d) - self.viewer = ScatterViewer(self.session) + self.viewer = self.app.new_data_viewer(ScatterViewer) self.data_collection.register_to_hub(self.hub) self.viewer.register_to_hub(self.hub) @@ -404,3 +405,39 @@ def test_all_options(self, ndim): layer_state.style = 'Line' layer_state.linewidth = 3 layer_state.linestyle = 'dashed' + + def test_session_categorical(self, tmpdir): + + def visible_xaxis_labels(ax): + # Due to a bug in Matplotlib the labels returned outside the field + # of view may be incorrect: https://github.com/matplotlib/matplotlib/issues/9397 + pos = ax.xaxis.get_ticklocs() + labels = [tick.get_text() for tick in ax.xaxis.get_ticklabels()] + xmin, xmax = ax.get_xlim() + return [labels[i] for i in range(len(pos)) if pos[i] >= xmin and pos[i] <= xmax] + + # Regression test for a bug that caused a restored scatter viewer + # with a categorical component to not show the categorical labels + # as tick labels. + + filename = tmpdir.join('test_session_categorical.glu').strpath + + self.viewer.add_data(self.data) + self.viewer.state.x_att = self.data.id['z'] + + assert visible_xaxis_labels(self.viewer.axes) == ['a', 'b', 'c'] + + self.session.application.save_session(filename) + + with open(filename, 'r') as f: + session = f.read() + + state = GlueUnSerializer.loads(session) + + ga = state.object('__main__') + + dc = ga.session.data_collection + + viewer = ga.viewers[0][0] + assert viewer.state.x_att is dc[0].id['z'] + assert visible_xaxis_labels(self.viewer.axes) == ['a', 'b', 'c'] From 3e3f2f28569a5b7731321054bbb36bacd420138e Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 14 Oct 2017 09:44:44 +0100 Subject: [PATCH 07/20] Fix warnings when running tests --- glue/core/component_link.py | 4 +- .../data_exporters/tests/test_gridded_fits.py | 7 +- glue/core/data_factories/fits.py | 19 ++++-- glue/core/data_factories/tests/test_fits.py | 27 ++++---- glue/core/subset.py | 3 +- glue/core/tests/test_subset.py | 3 +- glue/dialogs/link_editor/qt/link_equation.py | 8 ++- glue/plugins/dendro_viewer/data_factory.py | 66 +++++++++---------- glue/viewers/custom/qt/custom_viewer.py | 10 ++- 9 files changed, 82 insertions(+), 65 deletions(-) diff --git a/glue/core/component_link.py b/glue/core/component_link.py index 402974087..9e98ebbdb 100644 --- a/glue/core/component_link.py +++ b/glue/core/component_link.py @@ -152,8 +152,8 @@ def compute(self, data, view=None): result = np.asarray(result) logger.debug("shape of result: %s", result.shape) if result.shape != args[0].shape: - logger.warn("ComponentLink function %s changed shape. Fixing", - self._using.__name__) + logger.debug("ComponentLink function %s changed shape. Fixing", + self._using.__name__) result.shape = args[0].shape return result diff --git a/glue/core/data_exporters/tests/test_gridded_fits.py b/glue/core/data_exporters/tests/test_gridded_fits.py index 1b7d875ec..46e86d825 100644 --- a/glue/core/data_exporters/tests/test_gridded_fits.py +++ b/glue/core/data_exporters/tests/test_gridded_fits.py @@ -16,7 +16,6 @@ def test_fits_writer(tmpdir): fits_writer(filename, data) - hdulist = fits.open(filename) - - np.testing.assert_equal(hdulist['x'].data, data['x']) - np.testing.assert_equal(hdulist['y'].data, data['y']) + with fits.open(filename) as hdulist: + np.testing.assert_equal(hdulist['x'].data, data['x']) + np.testing.assert_equal(hdulist['y'].data, data['y']) diff --git a/glue/core/data_factories/fits.py b/glue/core/data_factories/fits.py index 6fafecc1e..53e3a44fd 100644 --- a/glue/core/data_factories/fits.py +++ b/glue/core/data_factories/fits.py @@ -52,11 +52,15 @@ def fits_reader(source, auto_merge=False, exclude_exts=None, label=None): from astropy.table import Table exclude_exts = exclude_exts or [] - if not isinstance(source, fits.hdu.hdulist.HDUList): + + if isinstance(source, fits.hdu.hdulist.HDUList): + hdulist = source + close_hdulist = False + else: hdulist = fits.open(source, ignore_missing_end=True) hdulist.verify('fix') - else: - hdulist = source + close_hdulist = True + groups = OrderedDict() extension_by_shape = OrderedDict() @@ -108,10 +112,7 @@ def new_data(): elif is_table_hdu(hdu): # Loop through columns and make component list table = Table.read(hdu, format='fits') - label = '{0}[{1}]'.format( - label_base, - hdu_name - ) + label = '{0}[{1}]'.format(label_base, hdu_name) data = Data(label=label) groups[hdu_name] = data for column_name in table.columns: @@ -122,6 +123,10 @@ def new_data(): component = Component.autotyped(column, units=column.unit) data.add_component(component=component, label=column_name) + + if close_hdulist: + hdulist.close() + return [groups[idx] for idx in groups] diff --git a/glue/core/data_factories/tests/test_fits.py b/glue/core/data_factories/tests/test_fits.py index c668f8c41..761101324 100644 --- a/glue/core/data_factories/tests/test_fits.py +++ b/glue/core/data_factories/tests/test_fits.py @@ -61,26 +61,27 @@ def test_container_fits(): # Check that fits_reader takes HDUList objects - hdulist = fits.open(os.path.join(DATA, 'generic.fits')) - d_set = fits_reader(hdulist) + with fits.open(os.path.join(DATA, 'generic.fits')) as hdulist: - _assert_equal_expected(d_set, expected) + d_set = fits_reader(hdulist) - # Sometimes the primary HDU is empty but with an empty array rather than - # None + _assert_equal_expected(d_set, expected) - hdulist[0].data = np.array([]) - d_set = fits_reader(hdulist) + # Sometimes the primary HDU is empty but with an empty array rather than + # None - _assert_equal_expected(d_set, expected) + hdulist[0].data = np.array([]) + d_set = fits_reader(hdulist) + + _assert_equal_expected(d_set, expected) - # Check that exclude_exts works + # Check that exclude_exts works - d_set = fits_reader(hdulist, exclude_exts=['TWOD']) - expected_reduced = deepcopy(expected) - expected_reduced.pop('generic[TWOD]') + d_set = fits_reader(hdulist, exclude_exts=['TWOD']) + expected_reduced = deepcopy(expected) + expected_reduced.pop('generic[TWOD]') - _assert_equal_expected(d_set, expected_reduced) + _assert_equal_expected(d_set, expected_reduced) @requires_astropy diff --git a/glue/core/subset.py b/glue/core/subset.py index 213979fa3..646523fdb 100644 --- a/glue/core/subset.py +++ b/glue/core/subset.py @@ -340,7 +340,8 @@ def write_mask(self, file_name, format="fits"): def read_mask(self, file_name): try: from astropy.io import fits - mask = fits.open(file_name)[0].data + with fits.open(file_name) as hdulist: + mask = hdulist[0].data except IOError: raise IOError("Could not read %s (not a fits file?)" % file_name) ind = np.where(mask.flat)[0] diff --git a/glue/core/tests/test_subset.py b/glue/core/tests/test_subset.py index 96713b787..ec4fe3522 100644 --- a/glue/core/tests/test_subset.py +++ b/glue/core/tests/test_subset.py @@ -321,7 +321,8 @@ def test_write(self): self.subset.write_mask(tmp) from astropy.io import fits - data = fits.open(tmp)[0].data + with fits.open(tmp) as hdulist: + data = hdulist[0].data expected = np.array([[0, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], diff --git a/glue/dialogs/link_editor/qt/link_equation.py b/glue/dialogs/link_editor/qt/link_equation.py index 9edadd529..500f94e41 100644 --- a/glue/dialogs/link_editor/qt/link_equation.py +++ b/glue/dialogs/link_editor/qt/link_equation.py @@ -1,7 +1,11 @@ from __future__ import absolute_import, division, print_function import os -from inspect import getargspec + +try: + from inspect import getfullargspec +except ImportError: # Python 2.7 + from inspect import getargspec as getfullargspec from qtpy import QtWidgets from qtpy import PYSIDE @@ -249,7 +253,7 @@ def _setup_editor_function(self): assert self.is_function() self.set_result_visible(True) func = self.function.function - args = getargspec(func)[0] + args = getfullargspec(func)[0] label = function_label(self.function) self._ui.info.setText(label) self._output_widget.label = self.function.output_labels[0] diff --git a/glue/plugins/dendro_viewer/data_factory.py b/glue/plugins/dendro_viewer/data_factory.py index 0d2391c22..a0d4db62e 100644 --- a/glue/plugins/dendro_viewer/data_factory.py +++ b/glue/plugins/dendro_viewer/data_factory.py @@ -32,49 +32,49 @@ def is_dendro(file, **kwargs): from astropy.io import fits - hdulist = fits.open(file, ignore_missing_end=True) + with fits.open(file, ignore_missing_end=True) as hdulist: - # In recent versions of Astropy, we could do 'DATA' in hdulist etc. but - # this doesn't work with Astropy 0.3, so we use the following method - # instead: - try: - hdulist['DATA'] - hdulist['INDEX_MAP'] - hdulist['NEWICK'] - except KeyError: - pass # continue - else: - return True + # In recent versions of Astropy, we could do 'DATA' in hdulist etc. but + # this doesn't work with Astropy 0.3, so we use the following method + # instead: + try: + hdulist['DATA'] + hdulist['INDEX_MAP'] + hdulist['NEWICK'] + except KeyError: + pass # continue + else: + return True - # For older versions of astrodendro, the HDUs did not have names + # For older versions of astrodendro, the HDUs did not have names - # Here we use heuristics to figure out if this is likely to be a - # dendrogram. Specifically, there should be three HDU extensions. - # The primary HDU should be empty, HDU 1 and HDU 2 should have - # matching shapes, and HDU 3 should have a 1D array. Also, if the - # HDUs do have names then this is not a dendrogram since the old - # files did not have names + # Here we use heuristics to figure out if this is likely to be a + # dendrogram. Specifically, there should be three HDU extensions. + # The primary HDU should be empty, HDU 1 and HDU 2 should have + # matching shapes, and HDU 3 should have a 1D array. Also, if the + # HDUs do have names then this is not a dendrogram since the old + # files did not have names - # This branch can be removed once we think most dendrogram files - # will have HDU names. + # This branch can be removed once we think most dendrogram files + # will have HDU names. - if len(hdulist) != 4: - return False + if len(hdulist) != 4: + return False - if hdulist[1].name != '' or hdulist[2].name != '' or hdulist[3].name != '': - return False + if hdulist[1].name != '' or hdulist[2].name != '' or hdulist[3].name != '': + return False - if hdulist[0].data is not None: - return False + if hdulist[0].data is not None: + return False - if hdulist[1].data is None or hdulist[2].data is None or hdulist[3].data is None: - return False + if hdulist[1].data is None or hdulist[2].data is None or hdulist[3].data is None: + return False - if hdulist[1].data.shape != hdulist[2].data.shape: - return False + if hdulist[1].data.shape != hdulist[2].data.shape: + return False - if hdulist[3].data.ndim != 1: - return False + if hdulist[3].data.ndim != 1: + return False # We're probably ok, so return True return True diff --git a/glue/viewers/custom/qt/custom_viewer.py b/glue/viewers/custom/qt/custom_viewer.py index e914efba8..bd5488520 100644 --- a/glue/viewers/custom/qt/custom_viewer.py +++ b/glue/viewers/custom/qt/custom_viewer.py @@ -67,7 +67,13 @@ from __future__ import print_function, division from functools import partial -from inspect import getmodule, getargspec +from inspect import getmodule + +try: + from inspect import getfullargspec +except ImportError: # Python 2.7 + from inspect import getargspec as getfullargspec + from types import FunctionType, MethodType from copy import copy @@ -240,7 +246,7 @@ def a(x, y): a(settings('x'), settings('y')) """ - a, k, _, _ = getargspec(func) + a, k = getfullargspec(func)[:2] try: # get the current values of each input to the UDF From 58b7a38724e426c7192fd4586c382fa8e94cfd74 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 14 Oct 2017 09:44:52 +0100 Subject: [PATCH 08/20] Fix test when layer or layer hub is not defined --- glue/viewers/matplotlib/state.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py index d31e6b536..3aae5ecec 100644 --- a/glue/viewers/matplotlib/state.py +++ b/glue/viewers/matplotlib/state.py @@ -95,4 +95,5 @@ def __init__(self, viewer_state=None, **kwargs): def _notify_layer_update(self, **kwargs): message = LayerArtistUpdatedMessage(self) - self.layer.hub.broadcast(message) + if self.layer is not None and self.layer.hub is not None: + self.layer.hub.broadcast(message) From c5ea677036a3913e0b53d1282eb2163cc7b37286 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 14 Oct 2017 09:59:05 +0100 Subject: [PATCH 09/20] Increase required QtPy version to 1.2 for Table viewer to work properly --- doc/installation/dependencies.rst | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/installation/dependencies.rst b/doc/installation/dependencies.rst index a9b3e1c8a..f41bc2eea 100644 --- a/doc/installation/dependencies.rst +++ b/doc/installation/dependencies.rst @@ -13,7 +13,7 @@ Glue has the following required dependencies: * `setuptools `_ 1.0 or later * Either `PySide `__ or `PyQt `__ (both PyQt4 and PyQt5 are supported) -* `QtPy `__ 1.1.1 or higher - this is an +* `QtPy `__ 1.2 or higher - this is an abstraction layer for the Python Qt packages * `IPython `_ 4.0 or higher * `ipykernel `_ diff --git a/setup.py b/setup.py index f1e3d9ec6..3b7207754 100755 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def run(self): 'pandas>=0.14', 'astropy>=1.3', 'matplotlib>=1.4', - 'qtpy>=1.1', + 'qtpy>=1.2', 'setuptools>=1.0', 'ipython>=4.0', 'ipykernel', From c36e3eab0ae70924e5975ddeea8facbd6f7055ec Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 14 Oct 2017 10:04:56 +0100 Subject: [PATCH 10/20] Show GUI error if adding N-dimensional data to Table viewer (where N > 1) --- glue/viewers/table/qt/data_viewer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py index 192178ed6..3e2bc9f71 100644 --- a/glue/viewers/table/qt/data_viewer.py +++ b/glue/viewers/table/qt/data_viewer.py @@ -20,7 +20,7 @@ from glue.core.edit_subset_mode import EditSubsetMode from glue.core.state import lookup_class_with_patches from glue.utils.colors import alpha_blend_colors -from glue.utils.qt import mpl_to_qt4_color +from glue.utils.qt import mpl_to_qt4_color, messagebox_on_error from glue.core.exceptions import IncompatibleAttribute __all__ = ['TableViewer', 'TableLayerArtist'] @@ -278,6 +278,7 @@ def _sync_layers(self): if subset not in self._layer_artist_container: self._layer_artist_container.append(TableLayerArtist(subset, self)) + @messagebox_on_error("Failed to add data") def add_data(self, data): self.data = data self.setUpdatesEnabled(False) From 9a56bed1c673a3e0350bc8196785792566a3baa4 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 14 Oct 2017 10:07:58 +0100 Subject: [PATCH 11/20] Fix rendering of error messages in Qt GUI --- glue/utils/qt/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/utils/qt/decorators.py b/glue/utils/qt/decorators.py index 9854c24c1..8038304a8 100644 --- a/glue/utils/qt/decorators.py +++ b/glue/utils/qt/decorators.py @@ -54,7 +54,7 @@ def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: - m = "%s%s%s" % (msg, sep, e.args[0]) + m = "%s%s%s" % (msg, sep, str(e)) detail = str(traceback.format_exc()) qmb = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Critical, "Error", m) qmb.setDetailedText(detail) From 6112d92bbe4caaf2a99571866eab6326ccb1a3f8 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 14 Oct 2017 10:09:40 +0100 Subject: [PATCH 12/20] Updated changelog --- CHANGES.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a3f2b5b1c..53ac34d55 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,21 +17,24 @@ v0.11.2 (unreleased) to be incorrectly calculated. [#1402] * Fixed icon for scatter plot layer when a colormap is used, and fix issues with - viewer layer icons not updating immediately. + viewer layer icons not updating immediately. [#1425] * Fixed dragging and dropping session files onto glue (this now loads the session rather than trying to load it as a dataset). Also now show a warning when - the application is about to be reset to open a new session. + the application is about to be reset to open a new session. [#1425] -* Make sure no errors happen if making a selection in an empty viewer. +* Make sure no errors happen if making a selection in an empty viewer. [#1425] -* Fix creating faceted subsets on Python 3.x when no dataset is selected. +* Fix creating faceted subsets on Python 3.x when no dataset is selected. [#1425] -* Fix issues with overlaying a scatter layer on an image. +* Fix issues with overlaying a scatter layer on an image. [#1425] * Fix issues with labels for categorical axes in the scatter and histogram viewers, in particular when loading viewers with categorical axes from - session files. + session files. [#1425] + +* Make sure a GUI error message is shown when adding non-1-dimensional data + to a table viewer. [#1425] v0.11.1 (2017-08-25) -------------------- From 9c5aae4c288cb91a280e3680fae073ca4e65bf11 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 14 Oct 2017 10:14:49 +0100 Subject: [PATCH 13/20] Fix typo --- glue/dialogs/link_editor/qt/link_equation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/dialogs/link_editor/qt/link_equation.py b/glue/dialogs/link_editor/qt/link_equation.py index 500f94e41..a564f7188 100644 --- a/glue/dialogs/link_editor/qt/link_equation.py +++ b/glue/dialogs/link_editor/qt/link_equation.py @@ -30,7 +30,7 @@ def function_label(function): :param function: A member from the glue.config.link_function registry """ - args = getargspec(function.function)[0] + args = getfullargspec(function.function)[0] args = ', '.join(args) output = function.output_labels output = ', '.join(output) From 7a02c974e05a6a2f719b4169f6517a5f34c2c8eb Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 16 Oct 2017 10:19:43 +0100 Subject: [PATCH 14/20] Make sure we use the most recent versions of packages by default on Travis and disable pip fallback --- .travis.yml | 14 ++++++-------- appveyor.yml | 1 + 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 955f637f7..e1df70cdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,18 +20,16 @@ env: - PYTHON_VERSION=3.6 global: # We add astropy-ci-extras to have the latest version of Astropy with older Numpy versions. - - CONDA_CHANNELS=astropy-ci-extras - - MATPLOTLIB_VERSION=1.5 - - NUMPY_VERSION=1.11 - - ASTROPY_VERSION=1.3 - - IPYTHON_VERSION=5 + - CONDA_CHANNELS="astropy-ci-extras astropy" - PYTEST_ARGS="--cov glue -vs" + - ASTROPY_VERSION=stable + - NUMPY_VERSION=stable - NO_CFG_FILES=false - QT_PKG=pyqt5 - SETUP_XVFB=True - - SPHINX_VERSION=1.5 - - CONDA_DEPENDENCIES="pip dill ipython matplotlib scipy cython h5py pygments pyzmq scikit-image pandas sphinx xlrd pillow pytest mock coverage pyyaml sphinx_rtd_theme qtpy traitlets ipykernel qtconsole" - - PIP_DEPENDENCIES="pytest-cov coveralls pyavm astrodendro awscli plotly spectral-cube" + - CONDA_DEPENDENCIES="pip dill ipython matplotlib scipy cython h5py pygments pyzmq scikit-image pandas sphinx xlrd pillow pytest mock coverage pyyaml sphinx_rtd_theme qtpy traitlets ipykernel qtconsole spectral-cube pytest-cov" + - PIP_DEPENDENCIES="coveralls pyavm astrodendro awscli plotly" + - PIP_FALLBACK=false - REMOVE_INSTALL_REQUIRES=0 - secure: NvQVc3XmmjXNVKrmaD31IgltsOImlnt3frAl4wU0pM223iejr7V57hz/V5Isx6sTANWEiRBMG27v2T8e5IiB7DQTxFUleZk3DWXQV1grw/GarEGUawXAgwDWpF0AE/7BRVJYqo2Elgaqf28+Jkun8ewvfPCiEROD2jWEpnZj+IQ= - secure: "SU9BYH8d9eNigypG3lC83s0NY6Mq9AHGKXyEGeXDtz1npJIC1KHdzPMP1v1K3dzCgl1p6ReMXPjZMCENyfNkad/xvzTzGk0Nu/4BjihrUPV6+ratVeLpv0JLm8ikh8q+sZURkdtzUOlds+Hfn5ku4LdpT87tcKHY9TINAGA34ZM=" diff --git a/appveyor.yml b/appveyor.yml index 8d7d276cf..dd6735eaf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,6 +12,7 @@ environment: # to the matrix section. CONDA_DEPENDENCIES: "astropy scipy cython pyqt matplotlib h5py pygments pyzmq scikit-image pandas xlrd pillow pytest mock coverage ipython ipykernel qtconsole traitlets qtpy" PIP_DEPENDENCIES: "plotly" + PIP_FALLBACK: "False" matrix: - PYTHON_VERSION: "2.7" From 8ae4b4de7f8e43c91822961df32508271cb1c8b3 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 16 Oct 2017 12:11:01 +0100 Subject: [PATCH 15/20] Remove unnecessary build --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index e1df70cdb..ae443700d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -72,10 +72,6 @@ matrix: # Test with older package versions: - - os: linux - env: PYTHON_VERSION=2.7 - QT_PKG=pyqt - - os: linux env: PYTHON_VERSION=2.7 MATPLOTLIB_VERSION=1.4 From 88a5a335016cc866eabab8bbe7923fb7c573f313 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 16 Oct 2017 12:11:14 +0100 Subject: [PATCH 16/20] Fix issue when trying to launch glue multiple times from Jupyter --- CHANGES.md | 3 +++ glue/app/qt/application.py | 34 ++++++++++++++++++++++++++++------ glue/app/qt/versions.py | 11 ++++------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 53ac34d55..08e29bd60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,9 @@ v0.11.2 (unreleased) * Make sure a GUI error message is shown when adding non-1-dimensional data to a table viewer. [#1425] +* Fix issues when trying to launch glue multiple times from a Jupyter session. + [#1425] + v0.11.1 (2017-08-25) -------------------- diff --git a/glue/app/qt/application.py b/glue/app/qt/application.py index d6c07a7eb..a5f843398 100644 --- a/glue/app/qt/application.py +++ b/glue/app/qt/application.py @@ -21,7 +21,7 @@ from glue.dialogs.data_wizard.qt import data_wizard from glue.dialogs.link_editor.qt import LinkEditor from glue.app.qt.edit_subset_mode_toolbar import EditSubsetModeToolBar -from glue.app.qt.mdi_area import GlueMdiArea, GlueMdiSubWindow +from glue.app.qt.mdi_area import GlueMdiArea from glue.app.qt.layer_tree_widget import PlotAction, LayerTreeWidget from glue.app.qt.preferences import PreferencesDialog from glue.viewers.common.qt.data_viewer import DataViewer @@ -33,7 +33,7 @@ from glue.app.qt.feedback import submit_bug_report, submit_feedback from glue.app.qt.plugin_manager import QtPluginManager -from glue.app.qt.versions import show_glue_info +from glue.app.qt.versions import QVersionsDialog from glue.app.qt.terminal import glue_terminal, IPythonTerminalError from glue.config import qt_fixed_layout_tab, qt_client, startup_action @@ -188,13 +188,22 @@ class GlueApplication(Application, QtWidgets.QMainWindow): def __init__(self, data_collection=None, session=None): + # At this point we need to check if a Qt application already exists - + # this happens for example if using the %gui qt/qt5 mode in Jupyter. We + # should keep a reference to the original icon so that we can restore it + # later + self._original_app = QtWidgets.QApplication.instance() + if self._original_app is not None: + self._original_icon = self._original_app.windowIcon() + + # Now we can get the application instance, which involves setting it + # up if it doesn't already exist. self.app = get_qapp() QtWidgets.QMainWindow.__init__(self) Application.__init__(self, data_collection=data_collection, session=session) - self.app.setQuitOnLastWindowClosed(True) icon = get_icon('app_icon') self.app.setWindowIcon(icon) @@ -478,7 +487,7 @@ def add_widget(self, new_widget, label=None, tab=None, return sub def _edit_settings(self): - self._editor = PreferencesDialog(self) + self._editor = PreferencesDialog(self, parent=self) self._editor.show() def gather_current_tab(self): @@ -574,7 +583,13 @@ def _create_menu(self): submenu.addAction(a) menu.addSeparator() menu.addAction("Edit &Preferences", self._edit_settings) - menu.addAction("&Quit", self.app.quit) + # Here we use close instead of self.app.quit because if we are launching + # glue from an environment with a Qt event loop already existing, we + # don't want to quit this. Using close here is safer, though it does + # mean that any dialog we launch from glue has to be either modal (to + # prevent quitting) or correctly define its parent so that it gets + # closed too. + menu.addAction("&Quit", self.close) mbar.addMenu(menu) menu = QtWidgets.QMenu(mbar) @@ -630,7 +645,12 @@ def _create_menu(self): menu.addAction(a) menu.addSeparator() - menu.addAction("Version information", show_glue_info) + menu.addAction("Version information", self._show_glue_info) + + def _show_glue_info(self): + window = QVersionsDialog(parent=self) + window.show() + window.exec_() def _choose_load_data(self, data_importer=None): if data_importer is None: @@ -1063,6 +1083,8 @@ def closeEvent(self, event): self._log.close() self._hub.broadcast(ApplicationClosedMessage(None)) event.accept() + if self._original_app is not None: + self._original_app.setWindowIcon(self._original_icon) def report_error(self, message, detail): """ diff --git a/glue/app/qt/versions.py b/glue/app/qt/versions.py index 98897cec4..94d316b7a 100644 --- a/glue/app/qt/versions.py +++ b/glue/app/qt/versions.py @@ -35,7 +35,7 @@ def __init__(self, *args, **kwargs): def _update_deps(self): status = get_status_as_odict() self._text = "" - for name, version in [('Glue', __version__)] + list(status.items()): + for name, version in [('Glue', __version__)] + list(status.items()): check = QtWidgets.QTreeWidgetItem(self.ui.version_tree.invisibleRootItem(), [name, version]) self._text += "{0}: {1}\n".format(name, version) @@ -53,13 +53,10 @@ def center(self): self.move(frameGm.topLeft()) -def show_glue_info(): - window = QVersionsDialog() - window.show() - window.exec_() - if __name__ == "__main__": from glue.utils.qt import get_qapp app = get_qapp() - show_glue_info() + window = QVersionsDialog() + window.show() + window.exec_() From 73d84af3e03037fc2dc4baa6e8d0a7c32a2c52e9 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 16 Oct 2017 12:16:44 +0100 Subject: [PATCH 17/20] Travis: try and fix documentation build --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index ae443700d..34b681f5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,6 +67,8 @@ matrix: env: PYTHON_VERSION=2.7 DOC_TRIGGER=1 APP_TRIGGER=1 + NUMPY_VERSION=1.11 + ASTROPY_VERSION=1.3 PYTEST_ARGS="--cov glue --no-optional-skip" NO_CFG_FILES=true From 5cc715ac35e49453aa0d58dfd409298b55dcb258 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 16 Oct 2017 13:49:26 +0100 Subject: [PATCH 18/20] Update link checking to use sphinx built-in link checking --- .travis.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 34b681f5d..84f23da8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,13 +62,9 @@ matrix: PIP_DEPENDENCIES="pytest-cov coveralls" REMOVE_INSTALL_REQUIRES=1 - # We need to keep the following on 2.7 because of linkchecker - os: linux - env: PYTHON_VERSION=2.7 + env: PYTHON_VERSION=3.6 DOC_TRIGGER=1 - APP_TRIGGER=1 - NUMPY_VERSION=1.11 - ASTROPY_VERSION=1.3 PYTEST_ARGS="--cov glue --no-optional-skip" NO_CFG_FILES=true @@ -121,9 +117,7 @@ before_install: - if [[ $QT_PKG == pyqt5 ]]; then export CONDA_DEPENDENCIES="pyqt=5 "$CONDA_DEPENDENCIES; fi # Documentation dependencies - # Note that we need to specify requests 2.9 because of a bug in the version check in linkchecker, - # and we can't use conda since requests 2.9 won't exist for e.g. Python 3.6 - - if [ $DOC_TRIGGER ]; then export PIP_DEPENDENCIES="sphinx-automodapi numpydoc linkchecker requests==2.9 "$PIP_DEPENDENCIES; fi + - if [ $DOC_TRIGGER ]; then export PIP_DEPENDENCIES="sphinx-automodapi numpydoc requests "$PIP_DEPENDENCIES; fi # Install ci-helpers and set up conda - git clone git://github.com/astropy/ci-helpers.git @@ -200,16 +194,15 @@ script: - if [[ $QT_PKG == False ]]; then glue --version; fi - - python setup.py test -a "$PYTEST_ARGS"; + - python setup.py test -a "$PYTEST_ARGS" # In the following, we use separate if statements for each line, to make # sure the exit code from each one is taken into account for the overall # exit code. - - if [ $DOC_TRIGGER ]; then cd doc; make html 2> warnings.log; cd ..; fi + - if [ $DOC_TRIGGER ]; then cd doc; make html linkcheck 2> warnings.log; cd ..; fi - if [ $DOC_TRIGGER ]; then cat doc/warnings.log; fi # make sure stderr was empty, i.e. no warnings - if [ $DOC_TRIGGER ]; then test ! -s doc/warnings.log; fi - - if [ $DOC_TRIGGER ]; then linkchecker --ignore-url=".*fontawesome.*" doc/_build/html; fi after_success: From 5005c34fe8bc56544a29649fcce416d46ca77e7a Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 16 Oct 2017 14:01:49 +0100 Subject: [PATCH 19/20] Update/fix some links in the documentation --- doc/customizing_guide/customization.rst | 2 +- doc/developer_guide/coding_guidelines.rst | 2 +- doc/developer_guide/testing.rst | 4 ++-- doc/faq.rst | 2 +- doc/getting_started/index.rst | 2 +- doc/gui_guide/3d_viewers.rst | 4 ++-- doc/gui_guide/spectrum.rst | 4 ++-- doc/installation/conda.rst | 2 +- doc/installation/dependencies.rst | 6 +++--- doc/installation/pip.rst | 2 +- doc/python_guide/data_tutorial.rst | 2 +- doc/videos.rst | 2 +- doc/whatsnew/0.11.rst | 2 +- glue/app/qt/splash_screen.py | 2 +- glue/app/qt/terminal.py | 4 ++-- glue/app/qt/versions.py | 2 +- glue/utils/array.py | 2 +- glue/utils/qt/delegates.py | 2 +- 18 files changed, 24 insertions(+), 24 deletions(-) diff --git a/doc/customizing_guide/customization.rst b/doc/customizing_guide/customization.rst index d8d326702..ae5d31c49 100644 --- a/doc/customizing_guide/customization.rst +++ b/doc/customizing_guide/customization.rst @@ -38,7 +38,7 @@ specify custom translation functions. Here's how: .. literalinclude:: scripts/config_link_example.py Some remarks about this code: - #. ``link_function`` is used as a `decorator `_. The decorator adds the function to Glue's list of link functions + #. ``link_function`` is used as a `decorator `_. The decorator adds the function to Glue's list of link functions #. We provide a short summary of the function in the ``info`` keyword, and a list of ``output_labels``. Usually, only one quantity is returned, so ``output_labels`` has one element. #. Glue will always pass numpy arrays as inputs to a link function, and expects a numpy array (or a tuple of numpy arrays) as output diff --git a/doc/developer_guide/coding_guidelines.rst b/doc/developer_guide/coding_guidelines.rst index f3804e29a..20ad83578 100644 --- a/doc/developer_guide/coding_guidelines.rst +++ b/doc/developer_guide/coding_guidelines.rst @@ -4,7 +4,7 @@ Coding guidelines Glue is written entirely in Python, and we abide by the following guidelines: * All code should be Python 2 and 3-compatible. We do this by using the `six - `_ package, which we bundle in + `_ package, which we bundle in ``glue.external.six``. * We follow many of the same guidelines as the `Astropy `_ project, which you can find `here `__. diff --git a/doc/developer_guide/testing.rst b/doc/developer_guide/testing.rst index cc086300c..46ba0388a 100644 --- a/doc/developer_guide/testing.rst +++ b/doc/developer_guide/testing.rst @@ -47,12 +47,12 @@ Continuous integration Every time someone opens a pull request to the Glue repository, and every time we merge changes into the code base, all the tests are run on `Travis -`_ and `AppVeyor `_. This is +`_ and `AppVeyor `_. This is referred to as *Continuous Integration*. One of the nice things about continuous integration is that it allows us to automatically run the tests for different operating systems, Python versions, versions of Numpy, and Qt frameworks (PyQt4, PyQt5, and PySide). -`Travis `_ runs tests on Linux and MacOS X, and `AppVeyor +`Travis `_ runs tests on Linux and MacOS X, and `AppVeyor `_ runs the tests on Windows. When you open a pull request, you will be able to check the status of the tests at the bottom, which will look something like this: diff --git a/doc/faq.rst b/doc/faq.rst index f18659a2a..002c04ae8 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -78,7 +78,7 @@ Something is broken, or confusing. What should I do? ---------------------------------------------------- If you think you've found a bug in Glue, feel free to add an issue to the -`GitHub issues page `_. If +`GitHub issues page `_. If you have general questions, feel free to post a message to the `Glue mailing list `_, or send us an `email `_ directly. diff --git a/doc/getting_started/index.rst b/doc/getting_started/index.rst index 428e4a1c0..7ed4529bf 100644 --- a/doc/getting_started/index.rst +++ b/doc/getting_started/index.rst @@ -40,7 +40,7 @@ There are multiple ways to open data: Find and open the file ``w5.fits`` which should be in the ``w5.tgz`` or ``w5.zip`` archive you downloaded above. This is a `WISE image `_ of the `W5 Star Forming Region - `_. While this is an astronomical + `_. While this is an astronomical dataset, glue can be used for data in any discipline, and many of the concepts shown below are applicable to many types of dataset. diff --git a/doc/gui_guide/3d_viewers.rst b/doc/gui_guide/3d_viewers.rst index 85756b3fc..49c44ee48 100644 --- a/doc/gui_guide/3d_viewers.rst +++ b/doc/gui_guide/3d_viewers.rst @@ -15,7 +15,7 @@ installed, you should see the 3D viewers in the list: :width: 339 If you don't see these in the list, then if you are using -`Anaconda `_ to manage your Python +`Anaconda `_ to manage your Python distribution, you can install the 3D viewers plugin using:: pip install glue-vispy-viewers @@ -97,7 +97,7 @@ experimental and currently very slow for displaying isosurfaces. In addition, it is only able to show a single isosurface level. We do not recommend using it at this time, and have disabled it by default. If you are interested in trying it out, see the `README.md -`_ file in +`_ file in the glue-vispy-viewers repository. Reporting issues diff --git a/doc/gui_guide/spectrum.rst b/doc/gui_guide/spectrum.rst index 3a24fb587..01d74cf29 100644 --- a/doc/gui_guide/spectrum.rst +++ b/doc/gui_guide/spectrum.rst @@ -151,7 +151,7 @@ parameter names. Each value is itself a dictionary with 4 entries: Astropy-based models ^^^^^^^^^^^^^^^^^^^^ -The :class:`~glue.core.fitters.AstropyFitter1D` base class can be subclassed to plug custom `astropy models and fitters `_ into Glue. This is very easy:: +The :class:`~glue.core.fitters.AstropyFitter1D` base class can be subclassed to plug custom `astropy models and fitters `_ into Glue. This is very easy:: from astropy.modeling import models, fitting @@ -181,4 +181,4 @@ of these ideas. .. figure:: images/emcee_screenshot.png :align: center - :width: 400 \ No newline at end of file + :width: 400 diff --git a/doc/installation/conda.rst b/doc/installation/conda.rst index 8f603ccc4..59ce0ca99 100644 --- a/doc/installation/conda.rst +++ b/doc/installation/conda.rst @@ -4,7 +4,7 @@ Anaconda Python Distribution (Recommended) **Platforms:** MacOS X, Linux, and Windows -We recommend using the `Anaconda `__ Python +We recommend using the `Anaconda `__ Python distribution from Continuum Analytics (or the related Miniconda distribution). Anaconda includes all of Glue's main dependencies. There are two ways of installing Glue with the Anaconda Python Distribution: :ref:`graphically using the diff --git a/doc/installation/dependencies.rst b/doc/installation/dependencies.rst index f41bc2eea..860431b1b 100644 --- a/doc/installation/dependencies.rst +++ b/doc/installation/dependencies.rst @@ -11,14 +11,14 @@ Glue has the following required dependencies: * `Pandas `_ 0.14 or later * `Astropy `_ 1.0 or higher * `setuptools `_ 1.0 or later -* Either `PySide `__ or `PyQt +* Either `PySide `__ or `PyQt `__ (both PyQt4 and PyQt5 are supported) * `QtPy `__ 1.2 or higher - this is an abstraction layer for the Python Qt packages * `IPython `_ 4.0 or higher * `ipykernel `_ * `qtconsole `_ -* `dill `_ 0.2 or later (which improves session saving) +* `dill `_ 0.2 or later (which improves session saving) * `h5py `_ 2.4 or later, for reading HDF5 files * `xlrd `_ 1.0 or later, for reading Excel files * `glue-vispy-viewers `_, which provide 3D viewers @@ -26,7 +26,7 @@ Glue has the following required dependencies: The following optional dependencies are also highly recommended and domain-independent: -* `SciPy `_ +* `SciPy `_ * `scikit-image `_ * `plotly `_ for exporting to plot.ly diff --git a/doc/installation/pip.rst b/doc/installation/pip.rst index 996679a6a..4287f6990 100644 --- a/doc/installation/pip.rst +++ b/doc/installation/pip.rst @@ -6,7 +6,7 @@ Installing with pip Installing glue with `pip `__ is possible, although you will need to first make sure that you install Qt and either `PyQt `__ or `PySide -`__, since these cannot be automatically installed with the +`__, since these cannot be automatically installed with the ``pip`` command. See the section on :ref:`installing-qt` for more details. Assuming that you have either PyQt or PySide installed, you can install glue diff --git a/doc/python_guide/data_tutorial.rst b/doc/python_guide/data_tutorial.rst index f48826bb5..a4b92f528 100644 --- a/doc/python_guide/data_tutorial.rst +++ b/doc/python_guide/data_tutorial.rst @@ -201,7 +201,7 @@ If you using the Glue application, you can then change the visual properties of This method of creating subsets can be a powerful technique. For a demo of using sending Scikit-learn-identified clusters back into Glue as subsets, see -`this notebook `_. +`this notebook `_. The following example demonstrates how to access subsets defined graphically in data viewers. Let's say that you have two subsets that you defined in the scatter plot and histogram data viewers: diff --git a/doc/videos.rst b/doc/videos.rst index b7da5faab..ae8e14348 100644 --- a/doc/videos.rst +++ b/doc/videos.rst @@ -46,7 +46,7 @@ Glue, FBI Crime Data, and Plotly (5 minutes) < -See also the `IPython notebook `_ that accompanies this video. +See also the `IPython notebook `_ that accompanies this video. Extracting slices from cubes ---------------------------- diff --git a/doc/whatsnew/0.11.rst b/doc/whatsnew/0.11.rst index 07433530c..445155662 100644 --- a/doc/whatsnew/0.11.rst +++ b/doc/whatsnew/0.11.rst @@ -96,7 +96,7 @@ Experimental WorldWide Telescope plugin --------------------------------------- We have developed a plugin that provides a `WorldWide Telescope (WWT) -`_ viewer inside glue: +`_ viewer inside glue: .. image:: images/v0.11/plugin_wwt.jpg :align: center diff --git a/glue/app/qt/splash_screen.py b/glue/app/qt/splash_screen.py index 6fc13f483..9d7a287d5 100644 --- a/glue/app/qt/splash_screen.py +++ b/glue/app/qt/splash_screen.py @@ -37,7 +37,7 @@ def paintEvent(self, event): def center(self): # Adapted from StackOverflow - # http://stackoverflow.com/questions/20243637/pyqt4-center-window-on-active-screen + # https://stackoverflow.com/questions/20243637/pyqt4-center-window-on-active-screen frameGm = self.frameGeometry() screen = QtWidgets.QApplication.desktop().screenNumber(QtWidgets.QApplication.desktop().cursor().pos()) centerPoint = QtWidgets.QApplication.desktop().screenGeometry(screen).center() diff --git a/glue/app/qt/terminal.py b/glue/app/qt/terminal.py index e312c4d98..1c4a9d2f2 100644 --- a/glue/app/qt/terminal.py +++ b/glue/app/qt/terminal.py @@ -2,11 +2,11 @@ A GUI Ipython terminal window which can interact with Glue. Based on code from - http://stackoverflow.com/a/9796491/1332492 + https://stackoverflow.com/a/9796491/1332492 and - http://stackoverflow.com/a/11525205/1332492 + https://stackoverflow.com/a/11525205/1332492 Usage: new_widget = glue_terminal(**kwargs) diff --git a/glue/app/qt/versions.py b/glue/app/qt/versions.py index 94d316b7a..4624470a4 100644 --- a/glue/app/qt/versions.py +++ b/glue/app/qt/versions.py @@ -45,7 +45,7 @@ def _copy(self): def center(self): # Adapted from StackOverflow - # http://stackoverflow.com/questions/20243637/pyqt4-center-window-on-active-screen + # https://stackoverflow.com/questions/20243637/pyqt4-center-window-on-active-screen frameGm = self.frameGeometry() screen = QtWidgets.QApplication.desktop().screenNumber(QtWidgets.QApplication.desktop().cursor().pos()) centerPoint = QtWidgets.QApplication.desktop().screenGeometry(screen).center() diff --git a/glue/utils/array.py b/glue/utils/array.py index 675473de2..1fa0fe3a1 100644 --- a/glue/utils/array.py +++ b/glue/utils/array.py @@ -17,7 +17,7 @@ def unbroadcast(array): Given an array, return a new array that is the smallest subset of the original array that can be re-broadcasted back to the original array. - See http://stackoverflow.com/questions/40845769/un-broadcasting-numpy-arrays + See https://stackoverflow.com/questions/40845769/un-broadcasting-numpy-arrays for more details. """ diff --git a/glue/utils/qt/delegates.py b/glue/utils/qt/delegates.py index 93c2efb71..63a68840c 100644 --- a/glue/utils/qt/delegates.py +++ b/glue/utils/qt/delegates.py @@ -13,7 +13,7 @@ class HtmlItemDelegate(QtWidgets.QStyledItemDelegate): """ # Implementation adapted based on solutions presented on StackOverflow: - # http://stackoverflow.com/questions/1956542/how-to-make-item-view-render-rich-html-text-in-qt + # https://stackoverflow.com/questions/1956542/how-to-make-item-view-render-rich-html-text-in-qt def paint(self, painter, option, index): From 7ad37198670956599ecfce29fb3521e525e8f961 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 16 Oct 2017 14:35:47 +0100 Subject: [PATCH 20/20] Travis: make sure there are no link errors --- .travis.yml | 4 ++++ doc/developer_guide/testing.rst | 4 ++-- doc/gui_guide/3d_viewers.rst | 4 ++-- doc/installation/dependencies.rst | 2 +- doc/installation/pip.rst | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 84f23da8a..5dc7c9b3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -204,6 +204,10 @@ script: # make sure stderr was empty, i.e. no warnings - if [ $DOC_TRIGGER ]; then test ! -s doc/warnings.log; fi + # Check for any broken links, ignore 'redirected with Found' + - if [ $DOC_TRIGGER ]; then grep -v "redirected with Found" doc/_build/linkcheck/output.txt > doc/_build/linkcheck/output_no_found_redirect.txt; fi + - if [ $DOC_TRIGGER ]; then test ! -s doc/_build/linkcheck/output_no_found_redirect.txt; fi + after_success: # Coverage testing diff --git a/doc/developer_guide/testing.rst b/doc/developer_guide/testing.rst index 46ba0388a..13607da77 100644 --- a/doc/developer_guide/testing.rst +++ b/doc/developer_guide/testing.rst @@ -47,13 +47,13 @@ Continuous integration Every time someone opens a pull request to the Glue repository, and every time we merge changes into the code base, all the tests are run on `Travis -`_ and `AppVeyor `_. This is +`_ and `AppVeyor `_. This is referred to as *Continuous Integration*. One of the nice things about continuous integration is that it allows us to automatically run the tests for different operating systems, Python versions, versions of Numpy, and Qt frameworks (PyQt4, PyQt5, and PySide). `Travis `_ runs tests on Linux and MacOS X, and `AppVeyor -`_ runs the tests on Windows. When you open a pull +`_ runs the tests on Windows. When you open a pull request, you will be able to check the status of the tests at the bottom, which will look something like this: diff --git a/doc/gui_guide/3d_viewers.rst b/doc/gui_guide/3d_viewers.rst index 49c44ee48..e3439e697 100644 --- a/doc/gui_guide/3d_viewers.rst +++ b/doc/gui_guide/3d_viewers.rst @@ -3,7 +3,7 @@ 3D viewers in Glue ================== -A plugin with 3D viewers for glue, powered by `VisPy `_, +A plugin with 3D viewers for glue, powered by `VisPy `_, is available. Provided that you installed glue with ``conda`` or with ``pip``, you should already have the 3D viewers available. You can check this by going to the **Canvas** menu in glue and selecting **New Data Viewer**, or alternatively @@ -104,6 +104,6 @@ Reporting issues ---------------- Please report any issues with the 3D viewers in the following `issue tracker -`_. Please first check that +`_. Please first check that there is not already a similar issue open -- if there is, please feel free to comment on that issue to let us know you ran into that problem too! diff --git a/doc/installation/dependencies.rst b/doc/installation/dependencies.rst index 860431b1b..f0f0a1145 100644 --- a/doc/installation/dependencies.rst +++ b/doc/installation/dependencies.rst @@ -7,7 +7,7 @@ Glue has the following required dependencies: * Python 2.7, or 3.3 and higher * `Numpy `_ 1.9 or later -* `Matplotlib `_ 1.4 or later +* `Matplotlib `_ 1.4 or later * `Pandas `_ 0.14 or later * `Astropy `_ 1.0 or higher * `setuptools `_ 1.0 or later diff --git a/doc/installation/pip.rst b/doc/installation/pip.rst index 4287f6990..61dbaaac9 100644 --- a/doc/installation/pip.rst +++ b/doc/installation/pip.rst @@ -3,7 +3,7 @@ Installing with pip **Platforms:** MacOS X, Linux, and Windows -Installing glue with `pip `__ is possible, although you +Installing glue with `pip `__ is possible, although you will need to first make sure that you install Qt and either `PyQt `__ or `PySide `__, since these cannot be automatically installed with the