diff --git a/.travis.yml b/.travis.yml index 955f637f7..5dc7c9b3b 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=" @@ -64,20 +62,14 @@ 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 PYTEST_ARGS="--cov glue --no-optional-skip" NO_CFG_FILES=true # 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 @@ -125,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 @@ -204,16 +194,19 @@ 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 + + # 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: diff --git a/CHANGES.md b/CHANGES.md index 31b6b6843..08e29bd60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,29 @@ 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. [#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. [#1425] + +* 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. [#1425] + +* 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. [#1425] + +* 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/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" 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..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 +`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..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 @@ -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,13 +97,13 @@ 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 ---------------- 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/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 a9b3e1c8a..f0f0a1145 100644 --- a/doc/installation/dependencies.rst +++ b/doc/installation/dependencies.rst @@ -7,18 +7,18 @@ 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 -* Either `PySide `__ or `PyQt +* 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 `_ * `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..61dbaaac9 100644 --- a/doc/installation/pip.rst +++ b/doc/installation/pip.rst @@ -3,10 +3,10 @@ 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 +`__, 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/application.py b/glue/app/qt/application.py index 49c570e2d..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: @@ -868,9 +888,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 +1050,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: @@ -1043,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/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) 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 98897cec4..4624470a4 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) @@ -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() @@ -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_() 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/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/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/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/dialogs/link_editor/qt/link_equation.py b/glue/dialogs/link_editor/qt/link_equation.py index 9edadd529..a564f7188 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 @@ -26,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) @@ -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/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/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) 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): 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/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 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/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): diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py index 6157d1a1b..3aae5ecec 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,10 @@ 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) + if self.layer is not None and self.layer.hub is not None: + 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/qt/data_viewer.py b/glue/viewers/scatter/qt/data_viewer.py index 043330c2e..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): @@ -64,9 +65,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..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) @@ -188,6 +189,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 @@ -398,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'] 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, 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) 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',