From a0af5d9640e169f462538b3cc97bace3fdde253f Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Wed, 11 Sep 2024 12:13:27 -0400 Subject: [PATCH 01/11] Start working on Qt scatter dialog tests. --- glue_ar/qt/export_tool.py | 30 +++++------ glue_ar/qt/tests/__init__.py | 0 glue_ar/qt/tests/test_dialog.py | 88 +++++++++++++++++++++++++++++++++ glue_ar/qt/tests/utils.py | 23 +++++++++ 4 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 glue_ar/qt/tests/__init__.py create mode 100644 glue_ar/qt/tests/test_dialog.py create mode 100644 glue_ar/qt/tests/utils.py diff --git a/glue_ar/qt/export_tool.py b/glue_ar/qt/export_tool.py index 10314da..eca8ce0 100644 --- a/glue_ar/qt/export_tool.py +++ b/glue_ar/qt/export_tool.py @@ -1,5 +1,4 @@ from os.path import splitext -from glue_vispy_viewers.volume.volume_viewer import VispyVolumeViewerMixin from qtpy import compat from qtpy.QtWidgets import QDialog @@ -8,7 +7,7 @@ from glue.viewers.common.tool import SimpleToolMenu, Tool from glue_qt.utils.threading import Worker -from glue_ar.utils import AR_ICON, xyz_bounds +from glue_ar.utils import AR_ICON, is_volume_viewer, xyz_bounds from glue_ar.common.export import export_viewer from glue_ar.qt.export_dialog import QtARExportDialog from glue_ar.qt.exporting_dialog import ExportingDialog @@ -52,20 +51,23 @@ def activate(self): if not export_path: return - _, ext = splitext(export_path) - ext = ext[1:] - filetype = _FILETYPE_NAMES.get(ext, None) layer_states = [layer.state for layer in self.viewer.layers if layer.enabled and layer.state.visible] - bounds = xyz_bounds(self.viewer.state, with_resolution=isinstance(self.viewer, VispyVolumeViewerMixin)) - - worker = Worker(export_viewer, - viewer_state=self.viewer.state, - layer_states=layer_states, - bounds=bounds, - state_dictionary=dialog.state_dictionary, - filepath=export_path, - compression=dialog.state.compression) + bounds = xyz_bounds(self.viewer.state, with_resolution=is_volume_viewer(self.viewer)) + + self._start_worker(export_viewer, + viewer_state=self.viewer.state, + layer_states=layer_states, + bounds=bounds, + state_dictionary=dialog.state_dictionary, + filepath=export_path, + compression=dialog.state.compression) + + def _start_worker(self, *args, **kwargs): + _, ext = splitext(kwargs["filepath"]) + ext = ext[1:] + filetype = _FILETYPE_NAMES.get(ext, None) + worker = Worker(*args, **kwargs) exporting_dialog = ExportingDialog(parent=self.viewer, filetype=filetype) worker.result.connect(exporting_dialog.close) worker.error.connect(exporting_dialog.close) diff --git a/glue_ar/qt/tests/__init__.py b/glue_ar/qt/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/glue_ar/qt/tests/test_dialog.py b/glue_ar/qt/tests/test_dialog.py new file mode 100644 index 0000000..519bd5d --- /dev/null +++ b/glue_ar/qt/tests/test_dialog.py @@ -0,0 +1,88 @@ +from itertools import product +from mock import patch +import pytest +from random import randint, random, seed +from typing import cast + +from glue.core import Data +from glue.core.link_helpers import LinkSame +from glue_qt.app import GlueApplication +from glue_vispy_viewers.scatter.qt.scatter_viewer import VispyScatterViewer + +from glue_ar.common.export import export_viewer +from glue_ar.common.scatter_export_options import ARVispyScatterExportOptions +from glue_ar.qt.export_dialog import QtARExportDialog +from glue_ar.qt.export_tool import QtARExportTool +from glue_ar.qt.tests.utils import dialog_auto_accept_with_options +from glue_ar.utils import export_label_for_layer + + +class TestScatterExportTool: + + def setup_method(self, method): + seed(1607) + self.app = GlueApplication() + size_1 = 20 + self.data_1 = Data(x=[random() for _ in range(size_1)], + y=[random() for _ in range(size_1)], + z=[random() for _ in range(size_1)], + label="Scatter Data 1") + self.app.data_collection.append(self.data_1) + size_2 = 35 + self.data_2 = Data(x=[randint(0, 10) for _ in range(size_2)], + y=[randint(-5, 5) for _ in range(size_2)], + z=[randint(100, 200) for _ in range(size_2)], + label="Scatter Data 2") + self.app.data_collection.append(self.data_2) + + for c in ('x', 'y', 'z'): + c1 = self.data_1.id[c] + c2 = self.data_2.id[c] + self.app.data_collection.add_link(LinkSame(c1, c2)) + + self.viewer: VispyScatterViewer = cast(VispyScatterViewer, self.app.new_data_viewer(VispyScatterViewer, data=self.data_1)) + self.viewer.add_data(self.data_2) + + def test_toolbar(self): + toolbar = self.viewer.toolbar + assert toolbar is not None + assert "save" in toolbar.tools + tool = toolbar.tools["save"] + assert len([subtool for subtool in tool.subtools if isinstance(subtool, QtARExportTool)]) == 1 + + @pytest.mark.parametrize("extension,compression", product(("glB", "glTF", "USDA", "USDC"), ("None", "Draco", "Meshoptimizer"))) + def test_tool_export_call(self, extension, compression): + auto_accept = dialog_auto_accept_with_options(filetype=extension, compression=compression) + with patch("qtpy.compat.getsavefilename") as fd, \ + patch.object(QtARExportDialog, "exec_", auto_accept), \ + patch.object(QtARExportTool, "_start_worker") as start_worker: + ext = extension.lower() + filepath = f"test.{ext}" + fd.return_value = filepath, ext + save_tool = self.viewer.toolbar.tools["save"] + ar_subtool = next(subtool for subtool in save_tool.subtools if isinstance(subtool, QtARExportTool)) + ar_subtool.activate() + + bounds = [ + (self.viewer.state.x_min, self.viewer.state.x_max), + (self.viewer.state.y_min, self.viewer.state.y_max), + (self.viewer.state.z_min, self.viewer.state.z_max), + ] + + # We can't use assert_called_once_with because the state dictionaries + # aren't recognized as equal + start_worker.assert_called_once() + call = start_worker.call_args_list[0] + assert call.args == (export_viewer,) + kwargs = call.kwargs + assert kwargs["viewer_state"] == self.viewer.state + assert kwargs["bounds"] == bounds + assert kwargs["filepath"] == filepath + assert kwargs["compression"] == compression + assert tuple(kwargs["state_dictionary"].keys()) == tuple(export_label_for_layer(layer) for layer in self.viewer.layers) + for value in kwargs["state_dictionary"].values(): + assert len(value) == 2 + assert value[0] == "Scatter" + assert isinstance(value[1], ARVispyScatterExportOptions) + assert value[1].theta_resolution == 8 + assert value[1].phi_resolution == 8 diff --git a/glue_ar/qt/tests/utils.py b/glue_ar/qt/tests/utils.py new file mode 100644 index 0000000..61e0013 --- /dev/null +++ b/glue_ar/qt/tests/utils.py @@ -0,0 +1,23 @@ +def auto_accept_selectdialog(*args): + def exec_replacement(dialog): + dialog.select_all() + dialog.accept() + return exec_replacement + + +def auto_accept_messagebox(*args): + def exec_replacement(box): + box.accept() + return exec_replacement + + +def dialog_auto_accept_with_options(**kwargs): + def exec_replacement(dialog): + if "layer" in kwargs: + dialog.state.layer = kwargs["layer"] + dialog.state.filetype = kwargs.get("filetype", "glTF") + dialog.state.compression = kwargs.get("compression", "None") + if "method" in kwargs: + dialog.state.method = kwargs["method"] + dialog.accept() + return exec_replacement From 8e8403aa125cdf462648cdaac259a18981b9cfab Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Wed, 11 Sep 2024 17:58:28 -0400 Subject: [PATCH 02/11] Start adding some tests of the Qt export dialog. --- glue_ar/qt/tests/test_dialog.py | 227 +++++++++++++++++++++----------- glue_ar/qt/tests/test_tool.py | 88 +++++++++++++ glue_ar/qt/tests/utils.py | 6 + 3 files changed, 246 insertions(+), 75 deletions(-) create mode 100644 glue_ar/qt/tests/test_tool.py diff --git a/glue_ar/qt/tests/test_dialog.py b/glue_ar/qt/tests/test_dialog.py index 519bd5d..cf0daf1 100644 --- a/glue_ar/qt/tests/test_dialog.py +++ b/glue_ar/qt/tests/test_dialog.py @@ -1,88 +1,165 @@ -from itertools import product -from mock import patch -import pytest -from random import randint, random, seed +from random import random, seed from typing import cast +from echo import CallbackProperty from glue.core import Data +from glue.core.state_objects import State from glue.core.link_helpers import LinkSame from glue_qt.app import GlueApplication -from glue_vispy_viewers.scatter.qt.scatter_viewer import VispyScatterViewer +from glue_vispy_viewers.volume.qt.volume_viewer import VispyVolumeViewer +from numpy import arange, ones +from qtpy.QtGui import QDoubleValidator, QIntValidator +from qtpy.QtWidgets import QCheckBox, QLabel, QLineEdit -from glue_ar.common.export import export_viewer -from glue_ar.common.scatter_export_options import ARVispyScatterExportOptions from glue_ar.qt.export_dialog import QtARExportDialog -from glue_ar.qt.export_tool import QtARExportTool -from glue_ar.qt.tests.utils import dialog_auto_accept_with_options -from glue_ar.utils import export_label_for_layer +from glue_ar.qt.tests.utils import combobox_options -class TestScatterExportTool: +class DummyState(State): + cb_int = CallbackProperty(0) + cb_float = CallbackProperty(1.7) + cb_bool = CallbackProperty(False) + + +class TestQtExportDialog: def setup_method(self, method): - seed(1607) + seed(102) self.app = GlueApplication() - size_1 = 20 - self.data_1 = Data(x=[random() for _ in range(size_1)], - y=[random() for _ in range(size_1)], - z=[random() for _ in range(size_1)], - label="Scatter Data 1") - self.app.data_collection.append(self.data_1) - size_2 = 35 - self.data_2 = Data(x=[randint(0, 10) for _ in range(size_2)], - y=[randint(-5, 5) for _ in range(size_2)], - z=[randint(100, 200) for _ in range(size_2)], - label="Scatter Data 2") - self.app.data_collection.append(self.data_2) - - for c in ('x', 'y', 'z'): - c1 = self.data_1.id[c] - c2 = self.data_2.id[c] + self.volume_data = Data( + label='Volume Data', + x=arange(24).reshape((2, 3, 4)), + y=ones((2, 3, 4)), + z=arange(100, 124).reshape((2, 3, 4))) + self.app.data_collection.append(self.volume_data) + + scatter_size = 50 + self.scatter_data= Data(x=[random() for _ in range(scatter_size)], + y=[random() for _ in range(scatter_size)], + z=[random() for _ in range(scatter_size)], + label="Scatter Data") + self.app.data_collection.append(self.scatter_data) + + # Link pixel axes to scatter + for i, c in enumerate(('x', 'y', 'z')): + ri = 2 - i + c1 = self.volume_data.id[f"Pixel Axis {ri} [{c}]"] + c2 = self.scatter_data.id[c] self.app.data_collection.add_link(LinkSame(c1, c2)) - - self.viewer: VispyScatterViewer = cast(VispyScatterViewer, self.app.new_data_viewer(VispyScatterViewer, data=self.data_1)) - self.viewer.add_data(self.data_2) - - def test_toolbar(self): - toolbar = self.viewer.toolbar - assert toolbar is not None - assert "save" in toolbar.tools - tool = toolbar.tools["save"] - assert len([subtool for subtool in tool.subtools if isinstance(subtool, QtARExportTool)]) == 1 - - @pytest.mark.parametrize("extension,compression", product(("glB", "glTF", "USDA", "USDC"), ("None", "Draco", "Meshoptimizer"))) - def test_tool_export_call(self, extension, compression): - auto_accept = dialog_auto_accept_with_options(filetype=extension, compression=compression) - with patch("qtpy.compat.getsavefilename") as fd, \ - patch.object(QtARExportDialog, "exec_", auto_accept), \ - patch.object(QtARExportTool, "_start_worker") as start_worker: - ext = extension.lower() - filepath = f"test.{ext}" - fd.return_value = filepath, ext - save_tool = self.viewer.toolbar.tools["save"] - ar_subtool = next(subtool for subtool in save_tool.subtools if isinstance(subtool, QtARExportTool)) - ar_subtool.activate() - - bounds = [ - (self.viewer.state.x_min, self.viewer.state.x_max), - (self.viewer.state.y_min, self.viewer.state.y_max), - (self.viewer.state.z_min, self.viewer.state.z_max), - ] - - # We can't use assert_called_once_with because the state dictionaries - # aren't recognized as equal - start_worker.assert_called_once() - call = start_worker.call_args_list[0] - assert call.args == (export_viewer,) - kwargs = call.kwargs - assert kwargs["viewer_state"] == self.viewer.state - assert kwargs["bounds"] == bounds - assert kwargs["filepath"] == filepath - assert kwargs["compression"] == compression - assert tuple(kwargs["state_dictionary"].keys()) == tuple(export_label_for_layer(layer) for layer in self.viewer.layers) - for value in kwargs["state_dictionary"].values(): - assert len(value) == 2 - assert value[0] == "Scatter" - assert isinstance(value[1], ARVispyScatterExportOptions) - assert value[1].theta_resolution == 8 - assert value[1].phi_resolution == 8 + + # We use a volume viewer because it can support both volume and scatter layers + self.viewer: VispyVolumeViewer = cast(VispyVolumeViewer, self.app.new_data_viewer(VispyVolumeViewer, data=self.volume_data)) + self.viewer.add_data(self.scatter_data) + + self.dialog = QtARExportDialog(parent=self.viewer, viewer=self.viewer) + self.dialog.show() + + def teardown_method(self, method): + self.dialog.close() + + def test_default_state(self): + state = self.dialog.state + assert state.filetype == "glB" + assert state.compression == "None" + assert state.layer == "Volume Data" + assert state.method in {"Isosurface", "Voxel"} + + assert state.filetype_helper.choices == ['glB', 'glTF', 'USDC', 'USDA'] + assert state.compression_helper.choices == ['None', 'Draco', 'Meshoptimizer'] + assert state.layer_helper.choices == ["Volume Data", "Scatter Data"] + assert set(state.method_helper.choices) == {"Isosurface", "Voxel"} + + def test_default_dictionary(self): + state_dict = self.dialog.state_dictionary + assert len(state_dict) == 2 + assert set(state_dict.keys()) == {"Volume Data", "Scatter Data"} + + def test_default_ui(self): + ui = self.dialog.ui + assert ui.button_cancel.isVisible() + assert ui.button_ok.isVisible() + assert ui.combosel_compression.isVisible() + assert ui.label_compression_message.isVisible() + + compression_options = combobox_options(ui.combosel_compression) + assert compression_options == ["None", "Draco", "Meshoptimizer"] + + def test_filetype_change(self): + state = self.dialog.state + ui = self.dialog.ui + + state.filetype = "USDC" + assert not ui.combosel_compression.isVisible() + assert not ui.label_compression_message.isVisible() + + state.filetype = "USDA" + assert not ui.combosel_compression.isVisible() + assert not ui.label_compression_message.isVisible() + + state.filetype = "glTF" + assert ui.combosel_compression.isVisible() + assert ui.label_compression_message.isVisible() + + state.filetype = "USDA" + assert not ui.combosel_compression.isVisible() + assert not ui.label_compression_message.isVisible() + + state.filetype = "glB" + assert ui.combosel_compression.isVisible() + assert ui.label_compression_message.isVisible() + + state.filetype = "glTF" + assert ui.combosel_compression.isVisible() + assert ui.label_compression_message.isVisible() + + def test_widgets_for_property(self): + state = DummyState() + + int_widgets = self.dialog._widgets_for_property(state, "cb_int", "Int CB") + assert len(int_widgets) == 2 + label, edit = int_widgets + assert isinstance(label, QLabel) + assert label.text() == "Int CB:" + assert isinstance(edit, QLineEdit) + assert isinstance(edit.validator(), QIntValidator) + assert edit.text() == "0" + + float_widgets = self.dialog._widgets_for_property(state, "cb_float", "Float CB") + assert len(float_widgets) == 2 + label, edit = float_widgets + assert isinstance(label, QLabel) + assert label.text() == "Float CB:" + assert isinstance(edit, QLineEdit) + assert isinstance(edit.validator(), QDoubleValidator) + assert edit.text() == "1.7" + + bool_widgets = self.dialog._widgets_for_property(state, "cb_bool", "Bool CB") + assert len(bool_widgets) == 1 + box = bool_widgets[0] + assert isinstance(box, QCheckBox) + assert box.text() == "Bool CB" + assert not box.isChecked() + + def test_update_layer_ui(self): + state = DummyState() + self.dialog._update_layer_ui(state) + assert self.dialog.ui.layer_layout.rowCount() == 3 + + def test_clear_layout(self): + self.dialog._clear_layer_layout() + assert self.dialog.ui.layer_layout.isEmpty() + assert self.dialog._layer_connections == [] + + def test_layer_change(self): + state = self.dialog.state + ui = self.dialog.ui + + state.layer = "Scatter Data" + assert state.method_helper.choices == ["Scatter"] + assert not ui.label_method.isVisible() + assert not ui.combosel_method.isVisible() + + state.layer = "Volume Data" + assert set(state.method_helper.choices) == {"Isosurface", "Voxel"} + assert ui.label_method.isVisible() + assert ui.combosel_method.isVisible() diff --git a/glue_ar/qt/tests/test_tool.py b/glue_ar/qt/tests/test_tool.py new file mode 100644 index 0000000..6e70733 --- /dev/null +++ b/glue_ar/qt/tests/test_tool.py @@ -0,0 +1,88 @@ +from itertools import product +from mock import patch +import pytest +from random import randint, random, seed +from typing import cast + +from glue.core import Data +from glue.core.link_helpers import LinkSame +from glue_qt.app import GlueApplication +from glue_vispy_viewers.scatter.qt.scatter_viewer import VispyScatterViewer + +from glue_ar.common.export import export_viewer +from glue_ar.common.scatter_export_options import ARVispyScatterExportOptions +from glue_ar.qt.export_dialog import QtARExportDialog +from glue_ar.qt.export_tool import QtARExportTool +from glue_ar.qt.tests.utils import dialog_auto_accept_with_options +from glue_ar.utils import export_label_for_layer + + +class TestScatterExportTool: + + def setup_method(self, method): + seed(1607) + self.app = GlueApplication() + size_1 = 20 + self.data_1 = Data(x=[random() for _ in range(size_1)], + y=[random() for _ in range(size_1)], + z=[random() for _ in range(size_1)], + label="Scatter Data 1") + self.app.data_collection.append(self.data_1) + size_2 = 35 + self.data_2 = Data(x=[randint(0, 10) for _ in range(size_2)], + y=[randint(-5, 5) for _ in range(size_2)], + z=[randint(100, 200) for _ in range(size_2)], + label="Scatter Data 2") + self.app.data_collection.append(self.data_2) + + for c in ('x', 'y', 'z'): + c1 = self.data_1.id[c] + c2 = self.data_2.id[c] + self.app.data_collection.add_link(LinkSame(c1, c2)) + + self.viewer: VispyScatterViewer = cast(VispyScatterViewer, self.app.new_data_viewer(VispyScatterViewer, data=self.data_1)) + self.viewer.add_data(self.data_2) + + def test_toolbar(self): + toolbar = self.viewer.toolbar + assert toolbar is not None + assert "save" in toolbar.tools + tool = toolbar.tools["save"] + assert len([subtool for subtool in tool.subtools if isinstance(subtool, QtARExportTool)]) == 1 + + @pytest.mark.parametrize("extension,compression", product(("glB", "glTF", "USDA", "USDC"), ("None", "Draco", "Meshoptimizer"))) + def test_tool_export_call(self, extension, compression): + auto_accept = dialog_auto_accept_with_options(filetype=extension, compression=compression) + with patch("qtpy.compat.getsavefilename") as fd, \ + patch.object(QtARExportDialog, "exec_", auto_accept), \ + patch.object(QtARExportTool, "_start_worker") as start_worker: + ext = extension.lower() + filepath = f"test.{ext}" + fd.return_value = filepath, ext + save_tool = self.viewer.toolbar.tools["save"] + ar_subtool = next(subtool for subtool in save_tool.subtools if isinstance(subtool, QtARExportTool)) + ar_subtool.activate() + + bounds = [ + (self.viewer.state.x_min, self.viewer.state.x_max), + (self.viewer.state.y_min, self.viewer.state.y_max), + (self.viewer.state.z_min, self.viewer.state.z_max), + ] + + # We can't use assert_called_once_with because the state dictionaries + # aren't recognized as equal + start_worker.assert_called_once() + call = start_worker.call_args_list[0] + assert call.args == (export_viewer,) + kwargs = call.kwargs + assert kwargs["viewer_state"] == self.viewer.state + assert kwargs["bounds"] == bounds + assert kwargs["filepath"] == filepath + assert kwargs["compression"] == compression + assert tuple(kwargs["state_dictionary"].keys()) == tuple(export_label_for_layer(layer) for layer in self.viewer.layers) + for value in kwargs["state_dictionary"].values(): + assert len(value) == 2 + assert value[0] == "Scatter" + assert isinstance(value[1], ARVispyScatterExportOptions) + assert value[1].theta_resolution == 8 + assert value[1].phi_resolution == 8 diff --git a/glue_ar/qt/tests/utils.py b/glue_ar/qt/tests/utils.py index 61e0013..234e9c6 100644 --- a/glue_ar/qt/tests/utils.py +++ b/glue_ar/qt/tests/utils.py @@ -1,3 +1,6 @@ +from qtpy.QtWidgets import QComboBox + + def auto_accept_selectdialog(*args): def exec_replacement(dialog): dialog.select_all() @@ -21,3 +24,6 @@ def exec_replacement(dialog): dialog.state.method = kwargs["method"] dialog.accept() return exec_replacement + +def combobox_options(box: QComboBox): + return [box.itemText(i) for i in range(box.count())] From ee5e6993c4b8f9f98a2bb05b8ae742c749f5cdbd Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Wed, 11 Sep 2024 17:58:58 -0400 Subject: [PATCH 03/11] Remove all the rows of a Qt form layout when clearing it. --- glue_ar/qt/export_dialog.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index af6cb02..4c67cba 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -7,7 +7,7 @@ from glue_qt.utils import load_ui from glue_ar.common.export_dialog_base import ARExportDialogBase -from qtpy.QtWidgets import QCheckBox, QDialog, QHBoxLayout, QLabel, QLayout, QLineEdit, QWidget +from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLayout, QLineEdit, QWidget from qtpy.QtGui import QIntValidator, QDoubleValidator @@ -65,6 +65,16 @@ def _clear_layout(self, layout: QLayout): else: self._clear_layout(item.layout()) + layout.removeItem(item) + + if isinstance(layout, QFormLayout): + self._clear_form_rows(layout) + + def _clear_form_rows(self, layout: QFormLayout): + if layout is not None: + while layout.rowCount(): + layout.removeRow(0) + def _clear_layer_layout(self): self._clear_layout(self.ui.layer_layout) From 3252a362b176452935a22557332acc5921100232 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 12 Sep 2024 15:59:17 -0400 Subject: [PATCH 04/11] Add tests of Jupyter dialog and fix a few minor issues. --- glue_ar/common/tests/test_base_dialog.py | 92 +++++++++++++++ glue_ar/jupyter/export_dialog.py | 10 +- glue_ar/jupyter/tests/__init__.py | 0 glue_ar/jupyter/tests/test_dialog.py | 140 +++++++++++++++++++++++ glue_ar/qt/export_dialog.py | 2 +- glue_ar/qt/tests/test_dialog.py | 74 ++++-------- 6 files changed, 257 insertions(+), 61 deletions(-) create mode 100644 glue_ar/common/tests/test_base_dialog.py create mode 100644 glue_ar/jupyter/tests/__init__.py create mode 100644 glue_ar/jupyter/tests/test_dialog.py diff --git a/glue_ar/common/tests/test_base_dialog.py b/glue_ar/common/tests/test_base_dialog.py new file mode 100644 index 0000000..322fc4b --- /dev/null +++ b/glue_ar/common/tests/test_base_dialog.py @@ -0,0 +1,92 @@ +from random import random, seed +from typing import Any + +from echo import CallbackProperty +from glue.core import Application, Data +from glue.core.link_helpers import LinkSame +from glue.core.state_objects import State +from numpy import arange, ones + + +class DummyState(State): + cb_int = CallbackProperty(0) + cb_float = CallbackProperty(1.7) + cb_bool = CallbackProperty(False) + + +class BaseExportDialogTest: + + app: Application + dialog: Any + + def _setup_data(self): + seed(102) + self.volume_data = Data( + label='Volume Data', + x=arange(24).reshape((2, 3, 4)), + y=ones((2, 3, 4)), + z=arange(100, 124).reshape((2, 3, 4))) + self.app.data_collection.append(self.volume_data) + + scatter_size = 50 + self.scatter_data= Data(x=[random() for _ in range(scatter_size)], + y=[random() for _ in range(scatter_size)], + z=[random() for _ in range(scatter_size)], + label="Scatter Data") + self.app.data_collection.append(self.scatter_data) + + # Link pixel axes to scatter + for i, c in enumerate(('x', 'y', 'z')): + ri = 2 - i + c1 = self.volume_data.id[f"Pixel Axis {ri} [{c}]"] + c2 = self.scatter_data.id[c] + self.app.data_collection.add_link(LinkSame(c1, c2)) + + def test_default_state(self): + state = self.dialog.state + assert state.filetype == "glB" + assert state.compression == "None" + assert state.layer == "Volume Data" + assert state.method in {"Isosurface", "Voxel"} + + assert state.filetype_helper.choices == ['glB', 'glTF', 'USDC', 'USDA'] + assert state.compression_helper.choices == ['None', 'Draco', 'Meshoptimizer'] + assert state.layer_helper.choices == ["Volume Data", "Scatter Data"] + assert set(state.method_helper.choices) == {"Isosurface", "Voxel"} + + def test_default_dictionary(self): + state_dict = self.dialog.state_dictionary + assert len(state_dict) == 2 + assert set(state_dict.keys()) == {"Volume Data", "Scatter Data"} + + def test_layer_change_state(self): + state = self.dialog.state + + state.layer = "Scatter Data" + assert state.method_helper.choices == ["Scatter"] + assert state.method == "Scatter" + + state.layer = "Volume Data" + assert set(state.method_helper.choices) == {"Isosurface", "Voxel"} + assert state.method in {"Isosurface", "Voxel"} + + state.layer = "Scatter Data" + assert state.method_helper.choices == ["Scatter"] + assert state.method == "Scatter" + + def test_method_settings_persistence(self): + state = self.dialog.state + + state.layer = "Volume Data" + state.method = "Voxel" + method, layer_export_state = self.dialog.state_dictionary["Volume Data"] + assert method == "Voxel" + layer_export_state.opacity_cutoff = 0.5 + + state.layer = "Scatter Data" + + state.layer = "Volume Data" + state.method = "Voxel" + method, layer_export_state = self.dialog.state_dictionary["Volume Data"] + assert method == "Voxel" + assert layer_export_state.opacity_cutoff == 0.5 diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index 72083f9..892a9fa 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -51,7 +51,6 @@ def vue_temp_rule(self, value): class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate): template_file = (__file__, "export_dialog.vue") - dialog_state = GlueState().tag(sync=True) dialog_open = traitlets.Bool().tag(sync=True) layer_items = traitlets.List().tag(sync=True) @@ -67,7 +66,7 @@ class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate): method_items = traitlets.List().tag(sync=True) method_selected = traitlets.Int().tag(sync=True) - layer_layout = traitlets.Instance(DOMWidget).tag(sync=True, **widget_serialization) + layer_layout = traitlets.Instance(v.Container).tag(sync=True, **widget_serialization) has_layer_options = traitlets.Bool().tag(sync=True) def __init__(self, @@ -76,7 +75,7 @@ def __init__(self, on_cancel: Optional[Callable] = None, on_export: Optional[Callable] = None): ARExportDialogBase.__init__(self, viewer=viewer) - self.layer_layout = VBox() + self.layer_layout = v.Container() VuetifyTemplate.__init__(self) self._on_layer_change(self.state.layer) @@ -89,7 +88,6 @@ def __init__(self, self.dialog_open = display self.on_cancel = on_cancel self.on_export = on_export - self.dialog_state = self.state self.input_widgets = [] @@ -123,14 +121,13 @@ def widgets_for_property(self, value = getattr(instance, property) t = type(value) - prop_name = self.display_name(property) if t is bool: widget = v.Checkbox(label=display_name) link((instance, property), (widget, 'value')) return [widget] elif t in (int, float): name = "integer" if t is int else "number" - widget = NumberField(type=t, label=prop_name, error_message=f"You must enter a valid {name}") + widget = NumberField(type=t, label=display_name, error_message=f"You must enter a valid {name}") link((instance, property), (widget, 'value'), lambda value: str(value), @@ -140,7 +137,6 @@ def widgets_for_property(self, return [] def vue_cancel_dialog(self, *args): - self.state = None self.state_dictionary = {} self.dialog_open = False if self.on_cancel: diff --git a/glue_ar/jupyter/tests/__init__.py b/glue_ar/jupyter/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/glue_ar/jupyter/tests/test_dialog.py b/glue_ar/jupyter/tests/test_dialog.py new file mode 100644 index 0000000..ecefd37 --- /dev/null +++ b/glue_ar/jupyter/tests/test_dialog.py @@ -0,0 +1,140 @@ +from unittest.mock import MagicMock +from typing import cast + +from glue_jupyter import JupyterApplication +from glue_jupyter.ipyvolume.volume import IpyvolumeVolumeView +# We can't use the Jupyter vispy widget for these tests until https://github.com/glue-viz/glue-vispy-viewers/pull/388 is released +# from glue_vispy_viewers.volume.jupyter.volume_viewer import JupyterVispyVolumeViewer +from ipyvuetify import Checkbox + +from glue_ar.common.tests.test_base_dialog import BaseExportDialogTest, DummyState +from glue_ar.jupyter.export_dialog import JupyterARExportDialog, NumberField + + +class TestJupyterExportDialog(BaseExportDialogTest): + + app: JupyterApplication + dialog: JupyterARExportDialog + + def setup_method(self, method): + self.app = JupyterApplication() + self._setup_data() + + # We use a volume viewer because it can support both volume and scatter layers + self.viewer: IpyvolumeVolumeView = cast(IpyvolumeVolumeView, self.app.volshow(widget="ipyvolume", data=self.volume_data)) + self.viewer.add_data(self.scatter_data) + + self.on_cancel = MagicMock() + self.on_export = MagicMock() + self.dialog = JupyterARExportDialog(viewer=self.viewer, display=True, + on_cancel=self.on_cancel, on_export=self.on_export) + + def teardown_method(self, method): + self.dialog.dialog_open = False + + def test_default_ui(self): + assert self.dialog.dialog_open + assert self.dialog.layer_items == [ + {"text": "Volume Data", "value": 0}, + {"text": "Scatter Data", "value": 1} + ] + assert self.dialog.layer_selected == 0 + assert self.dialog.compression_items == [ + {"text": "None", "value": 0}, + {"text": "Draco", "value": 1}, + {"text": "Meshoptimizer", "value": 2} + ] + assert self.dialog.compression_selected == 0 + assert self.dialog.filetype_items == [ + {"text": "glB", "value": 0}, + {"text": "glTF", "value": 1}, + {"text": "USDC", "value": 2}, + {"text": "USDA", "value": 3} + ] + assert self.dialog.filetype_selected == 0 + assert set([item["text"] for item in self.dialog.method_items]) == {"Isosurface", "Voxel"} + assert self.dialog.method_selected == 0 + assert self.dialog.has_layer_options + + def test_filetype_change(self): + state = self.dialog.state + + state.filetype = "USDC" + assert not self.dialog.show_compression + + state.filetype = "USDA" + assert not self.dialog.show_compression + + state.filetype = "glTF" + assert self.dialog.show_compression + + state.filetype = "USDA" + assert not self.dialog.show_compression + + state.filetype = "glB" + assert self.dialog.show_compression + + state.filetype = "glTF" + assert self.dialog.show_compression + + def test_widgets_for_property(self): + state = DummyState() + + int_widgets = self.dialog.widgets_for_property(state, "cb_int", "Int CB") + assert len(int_widgets) == 1 + widget = int_widgets[0] + assert isinstance(widget, NumberField) + assert widget.label == "Int CB" + assert widget.value == "0" + assert widget.number_type is int + assert widget.error_message == "You must enter a valid integer" + + float_widgets = self.dialog.widgets_for_property(state, "cb_float", "Float CB") + assert len(float_widgets) == 1 + widget = float_widgets[0] + assert isinstance(widget, NumberField) + assert widget.label == "Float CB" + assert widget.value == "1.7" + assert widget.number_type is float + assert widget.error_message == "You must enter a valid number" + + bool_widgets = self.dialog.widgets_for_property(state, "cb_bool", "Bool CB") + assert len(bool_widgets) == 1 + widget = bool_widgets[0] + assert isinstance(widget, Checkbox) + assert widget.label == "Bool CB" + assert widget.value is False + + def test_update_layer_ui(self): + state = DummyState() + self.dialog._update_layer_ui(state) + assert len(self.dialog.layer_layout.children) == 3 + + def test_layer_change_ui(self): + state = self.dialog.state + + state.layer = "Scatter Data" + assert self.dialog.method_selected == 0 + assert self.dialog.method_items == [{"text": "Scatter", "value": 0}] + assert not self.dialog.has_layer_options + + state.layer = "Volume Data" + assert self.dialog.method_items[self.dialog.method_selected]["text"] == state.method + assert set([item["text"] for item in self.dialog.method_items]) == {"Isosurface", "Voxel"} + assert self.dialog.has_layer_options + + state.layer = "Scatter Data" + assert self.dialog.method_selected == 0 + assert self.dialog.method_items == [{"text": "Scatter", "value": 0}] + assert not self.dialog.has_layer_options + + def test_on_cancel(self): + self.dialog.vue_cancel_dialog() + assert len(self.dialog.state_dictionary) == 0 + assert not self.dialog.dialog_open + self.on_cancel.assert_called_once_with() + + def test_on_export(self): + self.dialog.vue_export_viewer() + assert not self.dialog.dialog_open + self.on_export.assert_called_once_with() diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index 4c67cba..03c455b 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -77,6 +77,7 @@ def _clear_form_rows(self, layout: QFormLayout): def _clear_layer_layout(self): self._clear_layout(self.ui.layer_layout) + self._layer_connections = [] def _on_layer_change(self, layer_name: str): super()._on_layer_change(layer_name) @@ -86,7 +87,6 @@ def _on_layer_change(self, layer_name: str): def _update_layer_ui(self, state: State): self._clear_layer_layout() - self._layer_connections = [] for property in state.callback_properties(): row = QHBoxLayout() name = self.display_name(property) diff --git a/glue_ar/qt/tests/test_dialog.py b/glue_ar/qt/tests/test_dialog.py index cf0daf1..1c16f55 100644 --- a/glue_ar/qt/tests/test_dialog.py +++ b/glue_ar/qt/tests/test_dialog.py @@ -1,51 +1,24 @@ -from random import random, seed from typing import cast -from echo import CallbackProperty -from glue.core import Data -from glue.core.state_objects import State -from glue.core.link_helpers import LinkSame from glue_qt.app import GlueApplication from glue_vispy_viewers.volume.qt.volume_viewer import VispyVolumeViewer -from numpy import arange, ones from qtpy.QtGui import QDoubleValidator, QIntValidator from qtpy.QtWidgets import QCheckBox, QLabel, QLineEdit +from glue_ar.common.tests.test_base_dialog import BaseExportDialogTest, DummyState +from glue_ar.common.scatter_export_options import ARVispyScatterExportOptions from glue_ar.qt.export_dialog import QtARExportDialog from glue_ar.qt.tests.utils import combobox_options -class DummyState(State): - cb_int = CallbackProperty(0) - cb_float = CallbackProperty(1.7) - cb_bool = CallbackProperty(False) +class TestQtExportDialog(BaseExportDialogTest): - -class TestQtExportDialog: + app: GlueApplication + dialog: QtARExportDialog def setup_method(self, method): - seed(102) self.app = GlueApplication() - self.volume_data = Data( - label='Volume Data', - x=arange(24).reshape((2, 3, 4)), - y=ones((2, 3, 4)), - z=arange(100, 124).reshape((2, 3, 4))) - self.app.data_collection.append(self.volume_data) - - scatter_size = 50 - self.scatter_data= Data(x=[random() for _ in range(scatter_size)], - y=[random() for _ in range(scatter_size)], - z=[random() for _ in range(scatter_size)], - label="Scatter Data") - self.app.data_collection.append(self.scatter_data) - - # Link pixel axes to scatter - for i, c in enumerate(('x', 'y', 'z')): - ri = 2 - i - c1 = self.volume_data.id[f"Pixel Axis {ri} [{c}]"] - c2 = self.scatter_data.id[c] - self.app.data_collection.add_link(LinkSame(c1, c2)) + self._setup_data() # We use a volume viewer because it can support both volume and scatter layers self.viewer: VispyVolumeViewer = cast(VispyVolumeViewer, self.app.new_data_viewer(VispyVolumeViewer, data=self.volume_data)) @@ -57,23 +30,6 @@ def setup_method(self, method): def teardown_method(self, method): self.dialog.close() - def test_default_state(self): - state = self.dialog.state - assert state.filetype == "glB" - assert state.compression == "None" - assert state.layer == "Volume Data" - assert state.method in {"Isosurface", "Voxel"} - - assert state.filetype_helper.choices == ['glB', 'glTF', 'USDC', 'USDA'] - assert state.compression_helper.choices == ['None', 'Draco', 'Meshoptimizer'] - assert state.layer_helper.choices == ["Volume Data", "Scatter Data"] - assert set(state.method_helper.choices) == {"Isosurface", "Voxel"} - - def test_default_dictionary(self): - state_dict = self.dialog.state_dictionary - assert len(state_dict) == 2 - assert set(state_dict.keys()) == {"Volume Data", "Scatter Data"} - def test_default_ui(self): ui = self.dialog.ui assert ui.button_cancel.isVisible() @@ -145,21 +101,33 @@ def test_update_layer_ui(self): self.dialog._update_layer_ui(state) assert self.dialog.ui.layer_layout.rowCount() == 3 + state = ARVispyScatterExportOptions() + self.dialog._update_layer_ui(state) + assert self.dialog.ui.layer_layout.rowCount() == 2 + def test_clear_layout(self): self.dialog._clear_layer_layout() assert self.dialog.ui.layer_layout.isEmpty() assert self.dialog._layer_connections == [] - def test_layer_change(self): + def test_layer_change_ui(self): state = self.dialog.state ui = self.dialog.ui state.layer = "Scatter Data" - assert state.method_helper.choices == ["Scatter"] + assert ui.combosel_method.currentText() == state.method + assert combobox_options(ui.combosel_method) == ["Scatter"] assert not ui.label_method.isVisible() assert not ui.combosel_method.isVisible() state.layer = "Volume Data" - assert set(state.method_helper.choices) == {"Isosurface", "Voxel"} + assert set(combobox_options(ui.combosel_method)) == {"Isosurface", "Voxel"} + assert ui.combosel_method.currentText() == state.method assert ui.label_method.isVisible() assert ui.combosel_method.isVisible() + + state.layer = "Scatter Data" + assert ui.combosel_method.currentText() == state.method + assert combobox_options(ui.combosel_method) == ["Scatter"] + assert not ui.label_method.isVisible() + assert not ui.combosel_method.isVisible() From d5aa27dfbc52cfa551bd6f21442829de6d543d5a Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 12 Sep 2024 16:04:18 -0400 Subject: [PATCH 05/11] Add some method-switching to options persistence test. --- glue_ar/common/tests/test_base_dialog.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/glue_ar/common/tests/test_base_dialog.py b/glue_ar/common/tests/test_base_dialog.py index 322fc4b..10c5181 100644 --- a/glue_ar/common/tests/test_base_dialog.py +++ b/glue_ar/common/tests/test_base_dialog.py @@ -83,6 +83,15 @@ def test_method_settings_persistence(self): assert method == "Voxel" layer_export_state.opacity_cutoff = 0.5 + state.method = "Isosurface" + method, layer_export_state = self.dialog.state_dictionary["Volume Data"] + layer_export_state.isosurface_count = 25 + + state.method = "Voxel" + method, layer_export_state = self.dialog.state_dictionary["Volume Data"] + assert method == "Voxel" + assert layer_export_state.opacity_cutof == 0.5 + state.layer = "Scatter Data" state.layer = "Volume Data" @@ -90,3 +99,8 @@ def test_method_settings_persistence(self): method, layer_export_state = self.dialog.state_dictionary["Volume Data"] assert method == "Voxel" assert layer_export_state.opacity_cutoff == 0.5 + + state.method = "Isosurface" + method, layer_export_state = self.dialog.state_dictionary["Volume Data"] + assert method == "Isosurface" + assert layer_export_state.isosurface_count == 25 From c77ac5d6df01caf9d806b945b7b7b4ac5c53562a Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 12 Sep 2024 16:58:42 -0400 Subject: [PATCH 06/11] Add basic tests of volume tool export. --- .../{test_tool.py => test_tool_scatter.py} | 11 +- glue_ar/qt/tests/test_tool_volume.py | 102 ++++++++++++++++++ 2 files changed, 110 insertions(+), 3 deletions(-) rename glue_ar/qt/tests/{test_tool.py => test_tool_scatter.py} (92%) create mode 100644 glue_ar/qt/tests/test_tool_volume.py diff --git a/glue_ar/qt/tests/test_tool.py b/glue_ar/qt/tests/test_tool_scatter.py similarity index 92% rename from glue_ar/qt/tests/test_tool.py rename to glue_ar/qt/tests/test_tool_scatter.py index 6e70733..3a4d34a 100644 --- a/glue_ar/qt/tests/test_tool.py +++ b/glue_ar/qt/tests/test_tool_scatter.py @@ -1,8 +1,8 @@ from itertools import product -from mock import patch import pytest from random import randint, random, seed from typing import cast +from unittest.mock import patch from glue.core import Data from glue.core.link_helpers import LinkSame @@ -42,6 +42,10 @@ def setup_method(self, method): self.viewer: VispyScatterViewer = cast(VispyScatterViewer, self.app.new_data_viewer(VispyScatterViewer, data=self.data_1)) self.viewer.add_data(self.data_2) + + def teardown_method(self): + self.viewer.close(warn=False) + self.app.close() def test_toolbar(self): toolbar = self.viewer.toolbar @@ -79,8 +83,9 @@ def test_tool_export_call(self, extension, compression): assert kwargs["bounds"] == bounds assert kwargs["filepath"] == filepath assert kwargs["compression"] == compression - assert tuple(kwargs["state_dictionary"].keys()) == tuple(export_label_for_layer(layer) for layer in self.viewer.layers) - for value in kwargs["state_dictionary"].values(): + state_dict = kwargs["state_dictionary"] + assert tuple(state_dict.keys()) == ("Scatter Data 1", "Scatter Data 2") + for value in state_dict.values(): assert len(value) == 2 assert value[0] == "Scatter" assert isinstance(value[1], ARVispyScatterExportOptions) diff --git a/glue_ar/qt/tests/test_tool_volume.py b/glue_ar/qt/tests/test_tool_volume.py new file mode 100644 index 0000000..8428038 --- /dev/null +++ b/glue_ar/qt/tests/test_tool_volume.py @@ -0,0 +1,102 @@ +from itertools import product +from random import random, seed +from typing import cast +from unittest.mock import patch + +from glue.core import Data +from glue.core.link_helpers import LinkSame +from glue_qt.app import GlueApplication +from glue_vispy_viewers.volume.qt.volume_viewer import VispyVolumeViewer +from numpy import arange, ones +import pytest + +from glue_ar.common.export import export_viewer +from glue_ar.common.scatter_export_options import ARVispyScatterExportOptions +from glue_ar.common.volume_export_options import ARIsosurfaceExportOptions +from glue_ar.qt.export_dialog import QtARExportDialog +from glue_ar.qt.export_tool import QtARExportTool +from glue_ar.qt.tests.utils import dialog_auto_accept_with_options +from glue_ar.utils import export_label_for_layer + +class TestVolumeExportTool: + + def setup_method(self, method): + seed(18) + self.app = GlueApplication() + scatter_size = 18 + self.scatter_data = Data(x=[random() for _ in range(scatter_size)], + y=[random() for _ in range(scatter_size)], + z=[random() for _ in range(scatter_size)], + label="Scatter Data") + self.app.data_collection.append(self.scatter_data) + + self.volume_data = Data( + label='Volume Data', + x=arange(24).reshape((2, 3, 4)), + y=ones((2, 3, 4)), + z=arange(100, 124).reshape((2, 3, 4))) + self.app.data_collection.append(self.volume_data) + + # Link pixel axes to scatter + for i, c in enumerate(('x', 'y', 'z')): + ri = 2 - i + c1 = self.volume_data.id[f"Pixel Axis {ri} [{c}]"] + c2 = self.scatter_data.id[c] + self.app.data_collection.add_link(LinkSame(c1, c2)) + + self.viewer: VispyVolumeViewer = cast(VispyVolumeViewer, self.app.new_data_viewer(VispyVolumeViewer, data=self.volume_data)) + self.viewer.add_data(self.scatter_data) + + def teardown_method(self): + self.viewer.close(warn=False) + self.app.close() + + def test_toolbar(self): + toolbar = self.viewer.toolbar + assert toolbar is not None + assert "save" in toolbar.tools + tool = toolbar.tools["save"] + assert len([subtool for subtool in tool.subtools if isinstance(subtool, QtARExportTool)]) == 1 + + @pytest.mark.parametrize("extension,compression", product(("glB", "glTF", "USDA", "USDC"), ("None", "Draco", "Meshoptimizer"))) + def test_tool_export_call(self, extension, compression): + auto_accept = dialog_auto_accept_with_options(filetype=extension, compression=compression) + with patch("qtpy.compat.getsavefilename") as fd, \ + patch.object(QtARExportDialog, "exec_", auto_accept), \ + patch.object(QtARExportTool, "_start_worker") as start_worker: + ext = extension.lower() + filepath = f"test.{ext}" + fd.return_value = filepath, ext + save_tool = self.viewer.toolbar.tools["save"] + ar_subtool = next(subtool for subtool in save_tool.subtools if isinstance(subtool, QtARExportTool)) + ar_subtool.activate() + + bounds = [ + (self.viewer.state.x_min, self.viewer.state.x_max, self.viewer.state.resolution), + (self.viewer.state.y_min, self.viewer.state.y_max, self.viewer.state.resolution), + (self.viewer.state.z_min, self.viewer.state.z_max, self.viewer.state.resolution), + ] + + # We can't use assert_called_once_with because the state dictionaries + # aren't recognized as equal + start_worker.assert_called_once() + call = start_worker.call_args_list[0] + assert call.args == (export_viewer,) + kwargs = call.kwargs + assert kwargs["viewer_state"] == self.viewer.state + assert kwargs["bounds"] == bounds + assert kwargs["filepath"] == filepath + assert kwargs["compression"] == compression + state_dict = kwargs["state_dictionary"] + assert tuple(state_dict.keys()) == ("Volume Data", "Scatter Data") + + scatter_method, scatter_state = state_dict["Scatter Data"] + assert scatter_method == "Scatter" + assert isinstance(scatter_state, ARVispyScatterExportOptions) + assert scatter_state.theta_resolution == 8 + assert scatter_state.phi_resolution == 8 + + volume_method, volume_state = state_dict["Volume Data"] + assert volume_method == "Isosurface" + assert isinstance(volume_state, ARIsosurfaceExportOptions) + assert volume_state.isosurface_count == 20 From bfc3c087686ec3a52bb0811825385e0d24f83a30 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 12 Sep 2024 17:01:01 -0400 Subject: [PATCH 07/11] We don't need mock as a dependency; we're now using unittest.mock. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index b43366e..57e0c37 100644 --- a/setup.py +++ b/setup.py @@ -751,7 +751,6 @@ def data_files(root_directory): "flake8", "pytest", "pytest-cov", - "mock", ], "qt": [ "glue-qt", From 7f4fe443eadc5283934a7173dcfe6ca992051e51 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 12 Sep 2024 17:09:07 -0400 Subject: [PATCH 08/11] Codestyle fixes. --- glue_ar/common/tests/test_base_dialog.py | 12 ++-- glue_ar/jupyter/export_dialog.py | 3 +- glue_ar/jupyter/tests/test_dialog.py | 17 ++--- glue_ar/qt/tests/test_dialog.py | 3 +- glue_ar/qt/tests/test_tool_scatter.py | 76 +++++++++++----------- glue_ar/qt/tests/test_tool_volume.py | 83 ++++++++++++------------ glue_ar/qt/tests/utils.py | 1 + 7 files changed, 101 insertions(+), 94 deletions(-) diff --git a/glue_ar/common/tests/test_base_dialog.py b/glue_ar/common/tests/test_base_dialog.py index 10c5181..92aadf6 100644 --- a/glue_ar/common/tests/test_base_dialog.py +++ b/glue_ar/common/tests/test_base_dialog.py @@ -16,7 +16,7 @@ class DummyState(State): class BaseExportDialogTest: - app: Application + app: Application dialog: Any def _setup_data(self): @@ -29,10 +29,10 @@ def _setup_data(self): self.app.data_collection.append(self.volume_data) scatter_size = 50 - self.scatter_data= Data(x=[random() for _ in range(scatter_size)], - y=[random() for _ in range(scatter_size)], - z=[random() for _ in range(scatter_size)], - label="Scatter Data") + self.scatter_data = Data(x=[random() for _ in range(scatter_size)], + y=[random() for _ in range(scatter_size)], + z=[random() for _ in range(scatter_size)], + label="Scatter Data") self.app.data_collection.append(self.scatter_data) # Link pixel axes to scatter @@ -99,7 +99,7 @@ def test_method_settings_persistence(self): method, layer_export_state = self.dialog.state_dictionary["Volume Data"] assert method == "Voxel" assert layer_export_state.opacity_cutoff == 0.5 - + state.method = "Isosurface" method, layer_export_state = self.dialog.state_dictionary["Volume Data"] assert method == "Isosurface" diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index 892a9fa..9b66beb 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -1,6 +1,6 @@ import ipyvuetify as v # noqa from ipyvuetify.VuetifyTemplate import VuetifyTemplate -from ipywidgets import DOMWidget, VBox, widget_serialization +from ipywidgets import DOMWidget, widget_serialization import traitlets from typing import Callable, List, Optional @@ -8,7 +8,6 @@ from glue.core.state_objects import State from glue.viewers.common.viewer import Viewer from glue_jupyter.link import link -from glue_jupyter.state_traitlets_helpers import GlueState from glue_jupyter.vuetify_helpers import link_glue_choices from glue_ar.common.export_dialog_base import ARExportDialogBase diff --git a/glue_ar/jupyter/tests/test_dialog.py b/glue_ar/jupyter/tests/test_dialog.py index ecefd37..c7041df 100644 --- a/glue_ar/jupyter/tests/test_dialog.py +++ b/glue_ar/jupyter/tests/test_dialog.py @@ -2,9 +2,9 @@ from typing import cast from glue_jupyter import JupyterApplication +# We can't use the Jupyter vispy widget for these tests until +# https://github.com/glue-viz/glue-vispy-viewers/pull/388 is released from glue_jupyter.ipyvolume.volume import IpyvolumeVolumeView -# We can't use the Jupyter vispy widget for these tests until https://github.com/glue-viz/glue-vispy-viewers/pull/388 is released -# from glue_vispy_viewers.volume.jupyter.volume_viewer import JupyterVispyVolumeViewer from ipyvuetify import Checkbox from glue_ar.common.tests.test_base_dialog import BaseExportDialogTest, DummyState @@ -21,7 +21,8 @@ def setup_method(self, method): self._setup_data() # We use a volume viewer because it can support both volume and scatter layers - self.viewer: IpyvolumeVolumeView = cast(IpyvolumeVolumeView, self.app.volshow(widget="ipyvolume", data=self.volume_data)) + self.viewer: IpyvolumeVolumeView = cast(IpyvolumeVolumeView, + self.app.volshow(widget="ipyvolume", data=self.volume_data)) self.viewer.add_data(self.scatter_data) self.on_cancel = MagicMock() @@ -61,13 +62,13 @@ def test_filetype_change(self): state.filetype = "USDC" assert not self.dialog.show_compression - + state.filetype = "USDA" assert not self.dialog.show_compression state.filetype = "glTF" assert self.dialog.show_compression - + state.filetype = "USDA" assert not self.dialog.show_compression @@ -88,16 +89,16 @@ def test_widgets_for_property(self): assert widget.value == "0" assert widget.number_type is int assert widget.error_message == "You must enter a valid integer" - + float_widgets = self.dialog.widgets_for_property(state, "cb_float", "Float CB") assert len(float_widgets) == 1 widget = float_widgets[0] assert isinstance(widget, NumberField) assert widget.label == "Float CB" assert widget.value == "1.7" - assert widget.number_type is float + assert widget.number_type is float assert widget.error_message == "You must enter a valid number" - + bool_widgets = self.dialog.widgets_for_property(state, "cb_bool", "Bool CB") assert len(bool_widgets) == 1 widget = bool_widgets[0] diff --git a/glue_ar/qt/tests/test_dialog.py b/glue_ar/qt/tests/test_dialog.py index 1c16f55..b737491 100644 --- a/glue_ar/qt/tests/test_dialog.py +++ b/glue_ar/qt/tests/test_dialog.py @@ -21,7 +21,8 @@ def setup_method(self, method): self._setup_data() # We use a volume viewer because it can support both volume and scatter layers - self.viewer: VispyVolumeViewer = cast(VispyVolumeViewer, self.app.new_data_viewer(VispyVolumeViewer, data=self.volume_data)) + self.viewer: VispyVolumeViewer = cast(VispyVolumeViewer, + self.app.new_data_viewer(VispyVolumeViewer, data=self.volume_data)) self.viewer.add_data(self.scatter_data) self.dialog = QtARExportDialog(parent=self.viewer, viewer=self.viewer) diff --git a/glue_ar/qt/tests/test_tool_scatter.py b/glue_ar/qt/tests/test_tool_scatter.py index 3a4d34a..effa53e 100644 --- a/glue_ar/qt/tests/test_tool_scatter.py +++ b/glue_ar/qt/tests/test_tool_scatter.py @@ -1,4 +1,4 @@ -from itertools import product +from itertools import product import pytest from random import randint, random, seed from typing import cast @@ -14,7 +14,6 @@ from glue_ar.qt.export_dialog import QtARExportDialog from glue_ar.qt.export_tool import QtARExportTool from glue_ar.qt.tests.utils import dialog_auto_accept_with_options -from glue_ar.utils import export_label_for_layer class TestScatterExportTool: @@ -34,19 +33,20 @@ def setup_method(self, method): z=[randint(100, 200) for _ in range(size_2)], label="Scatter Data 2") self.app.data_collection.append(self.data_2) - + for c in ('x', 'y', 'z'): c1 = self.data_1.id[c] c2 = self.data_2.id[c] self.app.data_collection.add_link(LinkSame(c1, c2)) - - self.viewer: VispyScatterViewer = cast(VispyScatterViewer, self.app.new_data_viewer(VispyScatterViewer, data=self.data_1)) + + self.viewer: VispyScatterViewer = cast(VispyScatterViewer, + self.app.new_data_viewer(VispyScatterViewer, data=self.data_1)) self.viewer.add_data(self.data_2) def teardown_method(self): self.viewer.close(warn=False) self.app.close() - + def test_toolbar(self): toolbar = self.viewer.toolbar assert toolbar is not None @@ -54,40 +54,42 @@ def test_toolbar(self): tool = toolbar.tools["save"] assert len([subtool for subtool in tool.subtools if isinstance(subtool, QtARExportTool)]) == 1 - @pytest.mark.parametrize("extension,compression", product(("glB", "glTF", "USDA", "USDC"), ("None", "Draco", "Meshoptimizer"))) + @pytest.mark.parametrize("extension,compression", + product(("glB", "glTF", "USDA", "USDC"), ("None", "Draco", "Meshoptimizer"))) def test_tool_export_call(self, extension, compression): auto_accept = dialog_auto_accept_with_options(filetype=extension, compression=compression) with patch("qtpy.compat.getsavefilename") as fd, \ patch.object(QtARExportDialog, "exec_", auto_accept), \ patch.object(QtARExportTool, "_start_worker") as start_worker: - ext = extension.lower() - filepath = f"test.{ext}" - fd.return_value = filepath, ext - save_tool = self.viewer.toolbar.tools["save"] - ar_subtool = next(subtool for subtool in save_tool.subtools if isinstance(subtool, QtARExportTool)) - ar_subtool.activate() - - bounds = [ - (self.viewer.state.x_min, self.viewer.state.x_max), - (self.viewer.state.y_min, self.viewer.state.y_max), - (self.viewer.state.z_min, self.viewer.state.z_max), - ] - # We can't use assert_called_once_with because the state dictionaries - # aren't recognized as equal - start_worker.assert_called_once() - call = start_worker.call_args_list[0] - assert call.args == (export_viewer,) - kwargs = call.kwargs - assert kwargs["viewer_state"] == self.viewer.state - assert kwargs["bounds"] == bounds - assert kwargs["filepath"] == filepath - assert kwargs["compression"] == compression - state_dict = kwargs["state_dictionary"] - assert tuple(state_dict.keys()) == ("Scatter Data 1", "Scatter Data 2") - for value in state_dict.values(): - assert len(value) == 2 - assert value[0] == "Scatter" - assert isinstance(value[1], ARVispyScatterExportOptions) - assert value[1].theta_resolution == 8 - assert value[1].phi_resolution == 8 + ext = extension.lower() + filepath = f"test.{ext}" + fd.return_value = filepath, ext + save_tool = self.viewer.toolbar.tools["save"] + ar_subtool = next(subtool for subtool in save_tool.subtools if isinstance(subtool, QtARExportTool)) + ar_subtool.activate() + + bounds = [ + (self.viewer.state.x_min, self.viewer.state.x_max), + (self.viewer.state.y_min, self.viewer.state.y_max), + (self.viewer.state.z_min, self.viewer.state.z_max), + ] + + # We can't use assert_called_once_with because the state dictionaries + # aren't recognized as equal + start_worker.assert_called_once() + call = start_worker.call_args_list[0] + assert call.args == (export_viewer,) + kwargs = call.kwargs + assert kwargs["viewer_state"] == self.viewer.state + assert kwargs["bounds"] == bounds + assert kwargs["filepath"] == filepath + assert kwargs["compression"] == compression + state_dict = kwargs["state_dictionary"] + assert tuple(state_dict.keys()) == ("Scatter Data 1", "Scatter Data 2") + for value in state_dict.values(): + assert len(value) == 2 + assert value[0] == "Scatter" + assert isinstance(value[1], ARVispyScatterExportOptions) + assert value[1].theta_resolution == 8 + assert value[1].phi_resolution == 8 diff --git a/glue_ar/qt/tests/test_tool_volume.py b/glue_ar/qt/tests/test_tool_volume.py index 8428038..69f3352 100644 --- a/glue_ar/qt/tests/test_tool_volume.py +++ b/glue_ar/qt/tests/test_tool_volume.py @@ -16,7 +16,7 @@ from glue_ar.qt.export_dialog import QtARExportDialog from glue_ar.qt.export_tool import QtARExportTool from glue_ar.qt.tests.utils import dialog_auto_accept_with_options -from glue_ar.utils import export_label_for_layer + class TestVolumeExportTool: @@ -43,8 +43,9 @@ def setup_method(self, method): c1 = self.volume_data.id[f"Pixel Axis {ri} [{c}]"] c2 = self.scatter_data.id[c] self.app.data_collection.add_link(LinkSame(c1, c2)) - - self.viewer: VispyVolumeViewer = cast(VispyVolumeViewer, self.app.new_data_viewer(VispyVolumeViewer, data=self.volume_data)) + + self.viewer: VispyVolumeViewer = cast(VispyVolumeViewer, + self.app.new_data_viewer(VispyVolumeViewer, data=self.volume_data)) self.viewer.add_data(self.scatter_data) def teardown_method(self): @@ -58,45 +59,47 @@ def test_toolbar(self): tool = toolbar.tools["save"] assert len([subtool for subtool in tool.subtools if isinstance(subtool, QtARExportTool)]) == 1 - @pytest.mark.parametrize("extension,compression", product(("glB", "glTF", "USDA", "USDC"), ("None", "Draco", "Meshoptimizer"))) + @pytest.mark.parametrize("extension,compression", + product(("glB", "glTF", "USDA", "USDC"), ("None", "Draco", "Meshoptimizer"))) def test_tool_export_call(self, extension, compression): auto_accept = dialog_auto_accept_with_options(filetype=extension, compression=compression) with patch("qtpy.compat.getsavefilename") as fd, \ patch.object(QtARExportDialog, "exec_", auto_accept), \ patch.object(QtARExportTool, "_start_worker") as start_worker: - ext = extension.lower() - filepath = f"test.{ext}" - fd.return_value = filepath, ext - save_tool = self.viewer.toolbar.tools["save"] - ar_subtool = next(subtool for subtool in save_tool.subtools if isinstance(subtool, QtARExportTool)) - ar_subtool.activate() - - bounds = [ - (self.viewer.state.x_min, self.viewer.state.x_max, self.viewer.state.resolution), - (self.viewer.state.y_min, self.viewer.state.y_max, self.viewer.state.resolution), - (self.viewer.state.z_min, self.viewer.state.z_max, self.viewer.state.resolution), - ] - - # We can't use assert_called_once_with because the state dictionaries - # aren't recognized as equal - start_worker.assert_called_once() - call = start_worker.call_args_list[0] - assert call.args == (export_viewer,) - kwargs = call.kwargs - assert kwargs["viewer_state"] == self.viewer.state - assert kwargs["bounds"] == bounds - assert kwargs["filepath"] == filepath - assert kwargs["compression"] == compression - state_dict = kwargs["state_dictionary"] - assert tuple(state_dict.keys()) == ("Volume Data", "Scatter Data") - - scatter_method, scatter_state = state_dict["Scatter Data"] - assert scatter_method == "Scatter" - assert isinstance(scatter_state, ARVispyScatterExportOptions) - assert scatter_state.theta_resolution == 8 - assert scatter_state.phi_resolution == 8 - - volume_method, volume_state = state_dict["Volume Data"] - assert volume_method == "Isosurface" - assert isinstance(volume_state, ARIsosurfaceExportOptions) - assert volume_state.isosurface_count == 20 + + ext = extension.lower() + filepath = f"test.{ext}" + fd.return_value = filepath, ext + save_tool = self.viewer.toolbar.tools["save"] + ar_subtool = next(subtool for subtool in save_tool.subtools if isinstance(subtool, QtARExportTool)) + ar_subtool.activate() + + bounds = [ + (self.viewer.state.x_min, self.viewer.state.x_max, self.viewer.state.resolution), + (self.viewer.state.y_min, self.viewer.state.y_max, self.viewer.state.resolution), + (self.viewer.state.z_min, self.viewer.state.z_max, self.viewer.state.resolution), + ] + + # We can't use assert_called_once_with because the state dictionaries + # aren't recognized as equal + start_worker.assert_called_once() + call = start_worker.call_args_list[0] + assert call.args == (export_viewer,) + kwargs = call.kwargs + assert kwargs["viewer_state"] == self.viewer.state + assert kwargs["bounds"] == bounds + assert kwargs["filepath"] == filepath + assert kwargs["compression"] == compression + state_dict = kwargs["state_dictionary"] + assert tuple(state_dict.keys()) == ("Volume Data", "Scatter Data") + + scatter_method, scatter_state = state_dict["Scatter Data"] + assert scatter_method == "Scatter" + assert isinstance(scatter_state, ARVispyScatterExportOptions) + assert scatter_state.theta_resolution == 8 + assert scatter_state.phi_resolution == 8 + + volume_method, volume_state = state_dict["Volume Data"] + assert volume_method == "Isosurface" + assert isinstance(volume_state, ARIsosurfaceExportOptions) + assert volume_state.isosurface_count == 20 diff --git a/glue_ar/qt/tests/utils.py b/glue_ar/qt/tests/utils.py index 234e9c6..f817669 100644 --- a/glue_ar/qt/tests/utils.py +++ b/glue_ar/qt/tests/utils.py @@ -25,5 +25,6 @@ def exec_replacement(dialog): dialog.accept() return exec_replacement + def combobox_options(box: QComboBox): return [box.itemText(i) for i in range(box.count())] From 440e4646358bfeb914b9312c8aba586b9f2478ca Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 12 Sep 2024 17:21:17 -0400 Subject: [PATCH 09/11] Include Vue template for Jupyter export dialog in package data. --- MANIFEST.in | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 73b2513..194b6ef 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ recursive-include glue_ar/js * recursive-include glue_ar/resources * +include **/*.vue diff --git a/setup.py b/setup.py index 57e0c37..4cdd329 100644 --- a/setup.py +++ b/setup.py @@ -735,7 +735,7 @@ def data_files(root_directory): zip_safe=False, packages=find_packages(name, exclude=["js"]), package_data={ - "glue_ar": ["py.typed", "resources/**"], + "glue_ar": ["py.typed", "resources/**", "**/*.vue"], }, include_package_data=True, install_requires=[ From 566f2e1a4a8c0a1720ee6c49c3ef4088bbf79cd4 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 12 Sep 2024 17:58:11 -0400 Subject: [PATCH 10/11] Also include Qt UI files in package data. --- MANIFEST.in | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 194b6ef..bf1f6b6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ recursive-include glue_ar/js * recursive-include glue_ar/resources * include **/*.vue +include **/*.ui diff --git a/setup.py b/setup.py index 4cdd329..3a5d1f2 100644 --- a/setup.py +++ b/setup.py @@ -735,7 +735,7 @@ def data_files(root_directory): zip_safe=False, packages=find_packages(name, exclude=["js"]), package_data={ - "glue_ar": ["py.typed", "resources/**", "**/*.vue"], + "glue_ar": ["py.typed", "resources/**", "**/*.vue", "**/*.ui"], }, include_package_data=True, install_requires=[ From 0e6c8f6ed035f8783b13594c87a381b2bfc3be75 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 12 Sep 2024 18:09:33 -0400 Subject: [PATCH 11/11] Fix typo. --- glue_ar/common/tests/test_base_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue_ar/common/tests/test_base_dialog.py b/glue_ar/common/tests/test_base_dialog.py index 92aadf6..898c060 100644 --- a/glue_ar/common/tests/test_base_dialog.py +++ b/glue_ar/common/tests/test_base_dialog.py @@ -90,7 +90,7 @@ def test_method_settings_persistence(self): state.method = "Voxel" method, layer_export_state = self.dialog.state_dictionary["Volume Data"] assert method == "Voxel" - assert layer_export_state.opacity_cutof == 0.5 + assert layer_export_state.opacity_cutoff == 0.5 state.layer = "Scatter Data"