From 27e86abcaa2ba43cd55f1d80eb1a07156018f09f Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Thu, 16 May 2019 15:19:45 +0200 Subject: [PATCH 1/5] [test][assetwidget] Change target test experiment for a valid one --- beat/editor/test/test_assetwidget.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/beat/editor/test/test_assetwidget.py b/beat/editor/test/test_assetwidget.py index 7c91cc4..ab54641 100644 --- a/beat/editor/test/test_assetwidget.py +++ b/beat/editor/test/test_assetwidget.py @@ -59,10 +59,7 @@ def asset_type_prefix_entry_map(): AssetType.ALGORITHM: ("user/integers_add_v2/1", AlgorithmEditor), AssetType.DATABASE: ("integers_db/2", DatabaseEditor), AssetType.DATAFORMAT: ("user/single_string/1", DataformatEditor), - AssetType.EXPERIMENT: ( - "user/user/integers_echo/1/integers_echo", - ExperimentEditor, - ), + AssetType.EXPERIMENT: ("user/user/triangle/1/triangle", ExperimentEditor), AssetType.LIBRARY: ("user/sum/1", LibraryEditor), AssetType.PLOTTER: ("user/scatter/1", PlotterEditor), AssetType.PLOTTERPARAMETER: ("plot/config/1", PlotterParametersEditor), -- GitLab From 87b16741a422b715b6bdc26623d51c49934cc222 Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Thu, 23 May 2019 14:07:43 +0200 Subject: [PATCH 2/5] [widgets][spinboxes] Make value the user property like for QSpinBox This will allow use of the meta object system. --- beat/editor/test/test_parameterwidget.py | 4 +-- beat/editor/test/test_spinboxes.py | 36 +++++++++---------- beat/editor/widgets/parameterwidget.py | 34 +++++++++--------- .../editor/widgets/plotterparameterseditor.py | 2 +- beat/editor/widgets/spinboxes.py | 6 +++- 5 files changed, 43 insertions(+), 39 deletions(-) diff --git a/beat/editor/test/test_parameterwidget.py b/beat/editor/test/test_parameterwidget.py index 4a6ace1..d1f1928 100644 --- a/beat/editor/test/test_parameterwidget.py +++ b/beat/editor/test/test_parameterwidget.py @@ -470,8 +470,8 @@ class TestNumericalSetupWidget: with qtbot.waitSignal(numerical_setup_widget.dataChanged): qtbot.keyClicks(spinbox, value) - assert spinbox.value() == spinbox.numpy_type(expected_value) - default_state_dump["default"] = spinbox.value() + assert spinbox.value == spinbox.numpy_type(expected_value) + default_state_dump["default"] = spinbox.value assert numerical_setup_widget.dump() == default_state_dump def test_choices_click_add(self, qtbot, monkeypatch, numerical_setup_widget): diff --git a/beat/editor/test/test_spinboxes.py b/beat/editor/test/test_spinboxes.py index a5298d6..26a15d5 100644 --- a/beat/editor/test/test_spinboxes.py +++ b/beat/editor/test/test_spinboxes.py @@ -74,10 +74,10 @@ class SpinBoxBaseTest: assert spinbox.maximum() == maximum spinbox.setValue(-10) - assert spinbox.value() == spinbox.minimum() + assert spinbox.value == spinbox.minimum() spinbox.setValue(20) - assert spinbox.value() == spinbox.maximum() + assert spinbox.value == spinbox.maximum() def test_valid_input(self, qtbot): spinbox = NumpySpinBox(self.numpy_type) @@ -86,7 +86,7 @@ class SpinBoxBaseTest: with qtbot.waitSignal(spinbox.valueChanged): qtbot.keyClicks(spinbox, value) - assert spinbox.value() == spinbox.numpy_type(value) + assert spinbox.value == spinbox.numpy_type(value) def test_invalid_input(self, qtbot, expected_values): spinbox = NumpySpinBox(self.numpy_type) @@ -102,7 +102,7 @@ class SpinBoxBaseTest: with qtbot.waitSignal(spinbox.valueChanged): qtbot.keyClicks(spinbox, value) - assert spinbox.value() == spinbox.numpy_type(expected_value) + assert spinbox.value == spinbox.numpy_type(expected_value) class UintSpinBoxBaseTest(SpinBoxBaseTest): @@ -120,10 +120,10 @@ class UintSpinBoxBaseTest(SpinBoxBaseTest): spinbox.setRange(0, 10) spinbox.setValue(-10) - assert spinbox.value() == spinbox.minimum() + assert spinbox.value == spinbox.minimum() spinbox.setValue(20) - assert spinbox.value() == spinbox.maximum() + assert spinbox.value == spinbox.maximum() class IntSpinBoxBaseTest(SpinBoxBaseTest): @@ -134,10 +134,10 @@ class IntSpinBoxBaseTest(SpinBoxBaseTest): spinbox.setRange(0, 10) spinbox.setValue(5) - assert spinbox.value() == 5 + assert spinbox.value == 5 spinbox.setValue(-5) - assert spinbox.value() == spinbox.minimum() + assert spinbox.value == spinbox.minimum() class FloatSpinBoxBaseTest(SpinBoxBaseTest): @@ -148,16 +148,16 @@ class FloatSpinBoxBaseTest(SpinBoxBaseTest): spinbox.setRange(-10.45, 10.45) spinbox.setValue(5.32) - assert spinbox.value() == self.numpy_type(5.32) + assert spinbox.value == self.numpy_type(5.32) spinbox.setValue(-5.32) - assert spinbox.value() == self.numpy_type(-5.32) + assert spinbox.value == self.numpy_type(-5.32) spinbox.setValue(-15) - assert spinbox.value() == spinbox.minimum() + assert spinbox.value == spinbox.minimum() spinbox.setValue(15) - assert spinbox.value() == spinbox.maximum() + assert spinbox.value == spinbox.maximum() class TestUint8SpinBox(UintSpinBoxBaseTest): @@ -217,10 +217,10 @@ class TestTypeChange(SpinBoxBaseTest): spinbox.setRange(0, 10) spinbox.setValue(-10) - assert spinbox.value() == spinbox.minimum() + assert spinbox.value == spinbox.minimum() spinbox.setValue(20) - assert spinbox.value() == spinbox.maximum() + assert spinbox.value == spinbox.maximum() def test_values_from_uint8_to_float_64(self, qtbot): spinbox = NumpySpinBox(self.numpy_type) @@ -236,16 +236,16 @@ class TestTypeChange(SpinBoxBaseTest): spinbox.setRange(-10.45, 10.45) spinbox.setValue(5.32) - assert spinbox.value() == self.numpy_type(5.32) + assert spinbox.value == self.numpy_type(5.32) spinbox.setValue(-5.32) - assert spinbox.value() == self.numpy_type(-5.32) + assert spinbox.value == self.numpy_type(-5.32) spinbox.setValue(-15) - assert spinbox.value() == spinbox.minimum() + assert spinbox.value == spinbox.minimum() spinbox.setValue(15) - assert spinbox.value() == spinbox.maximum() + assert spinbox.value == spinbox.maximum() def test_type_change_with_same_type(self, qtbot): spinbox = NumpySpinBox(self.numpy_type) diff --git a/beat/editor/widgets/parameterwidget.py b/beat/editor/widgets/parameterwidget.py index 09ce1a6..0f243a6 100644 --- a/beat/editor/widgets/parameterwidget.py +++ b/beat/editor/widgets/parameterwidget.py @@ -161,7 +161,7 @@ class NumericalChoiceDialog(QDialog): def value(self): """Returns the value selected""" - return self.spinbox.value() + return self.spinbox.value @staticmethod def getChoiceValue(selected_type, parent=None): @@ -594,13 +594,13 @@ class NumericalSetupWidget(QWidget): for i in range(self.choices_listwidget.count()) ] elif self.range_button.isChecked(): - data["default"] = self.range_default_spinbox.value() + data["default"] = self.range_default_spinbox.value data["range"] = [ - self.range_minimum_spinbox.value(), - self.range_maximum_spinbox.value(), + self.range_minimum_spinbox.value, + self.range_maximum_spinbox.value, ] else: - data["default"] = self.single_default_spinbox.value() + data["default"] = self.single_default_spinbox.value return data @@ -633,9 +633,9 @@ class NumericalSetupWidget(QWidget): ensure that a range is really a range. """ - min_value = self.range_minimum_spinbox.value() - max_value = self.range_maximum_spinbox.value() - default_value = self.range_default_spinbox.value() + min_value = self.range_minimum_spinbox.value + max_value = self.range_maximum_spinbox.value + default_value = self.range_default_spinbox.value self.range_minimum_spinbox.setMaximum(max_value) self.range_maximum_spinbox.setMinimum(min_value) @@ -649,16 +649,16 @@ class NumericalSetupWidget(QWidget): self.range_default_spinbox.setValue(max_value) def restrict_range(self): - min_value = self.range_minimum_spinbox.value() - max_value = self.range_maximum_spinbox.value() - default_value = self.range_default_spinbox.value() + min_value = self.range_minimum_spinbox.value + max_value = self.range_maximum_spinbox.value + default_value = self.range_default_spinbox.value self.restrict_set_bounds(min_value, max_value, default_value) def restrict_range_from_min(self): - min_value = self.range_minimum_spinbox.value() - max_value = self.range_maximum_spinbox.value() - default_value = self.range_default_spinbox.value() + min_value = self.range_minimum_spinbox.value + max_value = self.range_maximum_spinbox.value + default_value = self.range_default_spinbox.value if min_value == max_value: self.range_maximum_spinbox.setMinimum(min_value) @@ -668,9 +668,9 @@ class NumericalSetupWidget(QWidget): self.restrict_set_bounds(min_value, max_value, default_value) def restrict_range_from_max(self): - min_value = self.range_minimum_spinbox.value() - max_value = self.range_maximum_spinbox.value() - default_value = self.range_default_spinbox.value() + min_value = self.range_minimum_spinbox.value + max_value = self.range_maximum_spinbox.value + default_value = self.range_default_spinbox.value if min_value == max_value: self.range_minimum_spinbox.setMaximum(max_value) diff --git a/beat/editor/widgets/plotterparameterseditor.py b/beat/editor/widgets/plotterparameterseditor.py index 1f2e8d0..f60986c 100644 --- a/beat/editor/widgets/plotterparameterseditor.py +++ b/beat/editor/widgets/plotterparameterseditor.py @@ -209,7 +209,7 @@ class RestrictedParameterWidget(QWidget): if self.modality == "choice": output = self.choices_combobox.currentText() else: - output = self.numerical_spinbox.value() + output = self.numerical_spinbox.value return output diff --git a/beat/editor/widgets/spinboxes.py b/beat/editor/widgets/spinboxes.py index 44782e9..66f3763 100644 --- a/beat/editor/widgets/spinboxes.py +++ b/beat/editor/widgets/spinboxes.py @@ -192,6 +192,10 @@ class NumpySpinBox(QAbstractSpinBox): self.valueChanged.emit() + value = pyqtProperty( + type="QVariant", fget=value, fset=setValue, notify=valueChanged, user=True + ) + def stepBy(self, steps): """Update the value of the spin box by steps. @@ -201,7 +205,7 @@ class NumpySpinBox(QAbstractSpinBox): :param steps int: step to increment/decrement the spin box """ - new_value = self.value() + new_value = self.value if steps < 0 and new_value + steps < self.minimum(): new_value = self.minimum() -- GitLab From 6827263c7fd45f2a9d94b2a2996d8cf378889d7e Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Fri, 23 Aug 2019 10:07:26 +0200 Subject: [PATCH 3/5] [widgets][assetbrowser] Automatically resize the first column when folder is loaded This make viewing the content easier. --- beat/editor/widgets/assetbrowser.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/beat/editor/widgets/assetbrowser.py b/beat/editor/widgets/assetbrowser.py index 6dd4244..9f18c88 100644 --- a/beat/editor/widgets/assetbrowser.py +++ b/beat/editor/widgets/assetbrowser.py @@ -113,9 +113,19 @@ class AssetBrowser(QWidget): layout = QVBoxLayout(self) layout.addWidget(self.view) + self.filesystem_model.directoryLoaded.connect(self.__onDirectoryLoaded) self.view.doubleClicked.connect(self._onItemDoubleClicked) self.view.customContextMenuRequested.connect(self._openMenu) + @pyqtSlot() + def __onDirectoryLoaded(self): + """When a directory is loaded, resize the first column. + + This makes the navigation easier. + """ + + self.view.resizeColumnToContents(0) + @pyqtSlot("QModelIndex") def _onItemDoubleClicked(self, index): """When an item is selected, emit the jsonSelected signal with -- GitLab From 14d86ddb7661acc574397e11cf73554526519c4f Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Fri, 23 Aug 2019 10:09:17 +0200 Subject: [PATCH 4/5] [resources] Added icon for IO remapping --- beat/editor/resources.py | 143 +++++++++++++++++++++++++++++++++++++++ editor.qrc | 5 ++ resources/remap.png | Bin 0 -> 1271 bytes 3 files changed, 148 insertions(+) create mode 100644 beat/editor/resources.py create mode 100644 editor.qrc create mode 100644 resources/remap.png diff --git a/beat/editor/resources.py b/beat/editor/resources.py new file mode 100644 index 0000000..faf6735 --- /dev/null +++ b/beat/editor/resources.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.9.6) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x04\xf7\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0e\xc4\x00\x00\x0e\xc4\ +\x01\x95\x2b\x0e\x1b\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ +\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x0e\x74\x45\ +\x58\x74\x54\x69\x74\x6c\x65\x00\x49\x4f\x20\x52\x65\x6d\x61\x70\ +\x67\x2e\x39\xbc\x00\x00\x00\x13\x74\x45\x58\x74\x41\x75\x74\x68\ +\x6f\x72\x00\x53\x61\x6d\x75\x65\x6c\x20\x47\x61\x69\x73\x74\xf3\ +\x89\x89\xa5\x00\x00\x00\x18\x74\x45\x58\x74\x43\x72\x65\x61\x74\ +\x69\x6f\x6e\x20\x54\x69\x6d\x65\x00\x32\x31\x2e\x30\x38\x2e\x32\ +\x30\x31\x39\x27\x0c\x38\x52\x00\x00\x00\x52\x74\x45\x58\x74\x43\ +\x6f\x70\x79\x72\x69\x67\x68\x74\x00\x43\x43\x20\x41\x74\x74\x72\ +\x69\x62\x75\x74\x69\x6f\x6e\x2d\x53\x68\x61\x72\x65\x41\x6c\x69\ +\x6b\x65\x20\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\x65\x61\x74\x69\ +\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\x67\x2f\x6c\x69\ +\x63\x65\x6e\x73\x65\x73\x2f\x62\x79\x2d\x73\x61\x2f\x34\x2e\x30\ +\x2f\xc3\x54\x62\x05\x00\x00\x03\xb9\x49\x44\x41\x54\x78\x9c\xed\ +\x9a\xcb\x6b\x14\x49\x1c\xc7\x3f\x35\xd3\x79\x88\x89\x84\x18\x45\ +\x03\x0b\xe3\x22\xa8\x87\xd1\xf1\x71\x08\x18\x75\xe6\x90\x8b\x88\ +\xeb\x65\xdd\xc3\xae\x0f\x04\x0f\x1e\x14\x02\x61\x0f\x7b\x51\xf0\ +\x24\x08\x39\xe4\x1f\x30\x28\x88\xe6\xa0\xde\x85\x69\x05\xc1\x83\ +\x8f\x59\xa2\xab\x7b\x31\x39\xc5\x15\x02\x59\x7c\xc5\x49\x7a\x2c\ +\x0f\x3d\x99\xed\xde\xf4\xcc\xd4\x74\x4f\x4f\x0f\x76\x7d\x86\x86\ +\xea\x5f\xfd\xaa\xe6\x37\xbf\xe9\xae\xea\xfa\x76\x81\x46\xa3\xd1\ +\x68\x34\x9a\xb8\x22\xe8\x62\x8e\x22\x9b\x15\xfd\xb7\x00\xb3\x21\ +\xc6\xd3\x1c\x7a\x7a\xee\xd2\xdd\x9d\xaa\xeb\x57\x2c\x2e\x19\x74\ +\xd2\xc9\x6b\xa0\x9e\xfb\x06\x3e\x32\xdf\x84\xe0\x5a\x81\x10\xc3\ +\x4c\x4c\xac\x67\xd3\xa6\xda\x7e\xc7\x8e\x2d\x1a\xad\x89\x28\x02\ +\x86\x86\x20\x95\xaa\xed\xd3\xd1\x51\x4a\xb4\x24\x98\x36\x46\xfd\ +\x0a\x48\x26\x93\x9c\xbf\xb2\x9f\xe1\xc3\x5b\xeb\xfa\x5a\xd6\x53\ +\x7e\xdd\xb9\x10\x24\x30\x65\x9e\xd3\x87\x41\xc6\x65\x3b\x48\x87\ +\x6a\x73\xf5\x04\x18\x5d\x6b\xd8\x9e\xbe\x81\xf8\x5a\xdf\xb7\x53\ +\xe4\x00\xb3\x7c\x96\x2d\x1f\x7e\x31\x1d\x7d\x79\xc4\x45\x06\x41\ +\xde\x65\x5b\x2b\x95\x3b\x6f\xc5\x18\x90\x3d\xb4\x99\x8b\xd9\xc1\ +\xc6\x1b\x9a\x73\xf0\xe0\xad\x5d\x6c\x6e\x48\xff\xa1\x9e\x00\xab\ +\xb8\xc8\xeb\xe9\xb3\x0c\xfc\xf0\xce\xb3\x3e\x21\x77\x21\xe5\x55\ +\xaf\xaa\xec\x20\x5c\xda\xdb\x78\x70\x97\xa8\x24\x40\x95\x51\x24\ +\x05\x3e\x89\x7b\xc0\x3a\x95\x06\xea\x09\x28\x95\x4a\x4c\x8c\x3d\ +\x62\x62\x6c\xd6\xb3\x7e\x6a\xda\x02\xa1\xdc\x5d\x28\x48\x0a\xa4\ +\x31\xe9\x65\x59\xb5\x89\xbf\x5b\xe0\xce\xf3\x3e\x2c\xc3\x3d\xf0\ +\x20\x32\xae\xf2\xd4\xb4\x5d\xfc\xfd\x78\x0a\x5e\xf9\xfa\x1a\x00\ +\x76\x90\xe2\x76\x8d\x31\x44\xfc\x6f\x00\x6c\x10\x83\x25\x96\xd8\ +\xa2\xe4\xdb\x53\x29\x59\x46\x06\x29\xf2\x55\x3d\x25\xe3\x95\xab\ +\x61\x5f\x0e\xde\x04\x48\x40\x8e\x53\x08\x4e\x35\xd4\xa6\x54\x2a\ +\x71\xe4\xc8\x7b\xba\xba\x6a\x8f\xd8\x0b\x0b\x3d\x06\x45\x7c\x0c\ +\x4f\x6d\xce\xe7\xcf\x3f\xf1\xf2\x65\xb7\x8a\x6b\xf0\x59\x40\x30\ +\x0a\xb2\x00\x22\x63\xff\xf3\x4e\x1b\xf0\x24\x7f\x9a\xfe\x06\xff\ +\x41\x27\x79\x26\x39\xc7\x35\x25\x5f\x8b\x42\xb9\xf4\x58\xb5\xfb\ +\x26\x4c\x83\xb2\xc0\xcf\x69\xd3\xbe\xe7\x85\xdb\x66\x93\xa5\x3f\ +\x40\xf7\xaf\x98\x25\x1d\xde\x34\x18\xfb\x47\x61\x9d\x80\xa8\x03\ +\x88\x1a\x9d\x80\xa8\x03\x88\x9a\xd8\x27\xc0\xd0\x9a\xa0\xd6\x04\ +\xbf\x53\xb4\x26\xa8\x86\x4f\x4d\x50\xee\x82\x15\xd9\x29\xb1\x97\ +\x5b\x7f\x19\xde\x36\xe0\x8f\xe3\x3f\xc2\x0b\xff\x11\x56\x5b\x0e\ +\x5b\x14\xd8\xcd\xbf\xd1\x68\x82\x4e\xc9\x4d\xca\xab\x08\xe9\x6d\ +\x03\xd8\x73\x10\xde\x04\x48\x40\xb5\xe5\xb0\x81\xad\x3b\x06\xd4\ +\x04\x63\x7f\x0b\xa8\x27\xc0\xd6\x04\x7f\x43\x26\x46\x10\x62\xac\ +\x62\x17\x62\xac\xaa\x4d\x26\x46\x78\xf6\xf0\x7a\xa0\x08\xf3\x4c\ +\x22\xc9\x21\xc9\x01\xa3\x75\xbc\x47\x91\xe4\xf8\x24\xde\xab\x76\ +\xef\x4f\x13\x74\xe9\x7f\x5f\x9f\xf2\x4b\xda\xf4\xb4\xd9\x0c\xe3\ +\x43\x10\xad\xe0\x5c\x0e\x3b\x57\xdc\x5e\xf8\xd0\x04\x63\x7f\x0b\ +\xf8\xd3\x04\xdb\x1d\xad\x09\xb6\x52\x13\x6c\x4f\x94\x35\xc1\xd8\ +\x8f\x01\x3a\x01\x51\x07\x10\x35\x3a\x01\x51\x07\x10\x35\xb1\x4f\ +\x40\x48\xd3\x60\x48\xcb\x61\xe7\x9b\x60\x41\xa6\xfc\x68\x1c\xf0\ +\xed\x70\x18\x9a\x60\x2b\x96\xc3\x30\x5e\x75\x5d\xa0\x35\xc1\x28\ +\x34\xc1\x25\xf9\x27\x46\x72\x64\x95\xfd\xd9\xc3\x93\xf4\x71\xc2\ +\x77\xbf\xce\xb7\xc3\xf6\xe5\x3e\x5e\xae\xb1\xb7\xc3\x38\xb1\x1c\ +\xe7\x8a\x9a\x60\xf3\x12\x60\x6f\x8b\xbb\xef\x51\x13\xce\x72\x78\ +\x65\xe9\x1b\x10\xf5\x04\x24\x12\x49\x2e\x5c\x1e\xe2\xc0\xd1\x94\ +\x7b\x3b\x4c\x44\xac\x0c\x82\x5e\x84\xa2\x09\x76\x74\xaf\x61\xdb\ +\xbe\x9b\xc8\xc6\x37\x42\x99\x73\xf6\x8e\x2f\x3f\xed\x6a\x50\x7d\ +\x10\x6c\xb3\x7d\x82\xe6\x83\xb7\x0d\x6f\x77\x73\xb5\x6f\x5e\x28\ +\xab\x51\x4f\xc0\xf2\x97\x45\xfe\x7e\x72\x86\x8d\x83\xff\xb8\x7b\ +\xb0\x0a\x55\x5a\xac\x60\xd2\xac\x1f\x61\x51\x28\xab\xc1\xb5\x69\ +\x60\x9f\x20\xf4\x32\xcf\x0c\xb2\xee\x67\x80\x0f\xd4\x9f\x2c\xdb\ +\x83\xde\xde\x79\x66\x66\x24\x52\xd6\x3e\x06\x06\x3e\xe8\x47\xe1\ +\xb8\x6b\x82\x11\xef\x6d\x0d\x8d\x21\x40\x49\x13\xd4\x68\x34\xf1\ +\xe6\x1b\x73\x4e\x70\x5b\x16\x15\xc8\x8a\x00\x00\x00\x00\x49\x45\ +\x4e\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x09\ +\x0a\x6c\x78\x43\ +\x00\x72\ +\x00\x65\x00\x73\x00\x6f\x00\x75\x00\x72\x00\x63\x00\x65\x00\x73\ +\x00\x09\ +\x03\x83\xa6\xc7\ +\x00\x72\ +\x00\x65\x00\x6d\x00\x61\x00\x70\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x6c\xb3\xa2\x85\xe8\ +" + +qt_version = QtCore.qVersion().split(".") +if qt_version < ["5", "8", "0"]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + + +def qInitResources(): + QtCore.qRegisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + + +def qCleanupResources(): + QtCore.qUnregisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + + +qInitResources() diff --git a/editor.qrc b/editor.qrc new file mode 100644 index 0000000..44a2845 --- /dev/null +++ b/editor.qrc @@ -0,0 +1,5 @@ + + + resources/remap.png + + diff --git a/resources/remap.png b/resources/remap.png new file mode 100644 index 0000000000000000000000000000000000000000..dbe945cc12dab76dbde238e241210637e557fafd GIT binary patch literal 1271 zcmVKrX>)Y*iHW5E000V>IRB3Hx05UNyFgPwUFflnN3^-B% z002^SMObu0Z*X~XX=iA307F9{L3DI-X<~JBX>V>VQ)ppwWkGCdYh@s4baZe!FE3+q zWnpw_c4cF4ZEbIEb1rXkXD@7NV`Xl0WpgiLc`b8cFElPNFT+$~1poj8xk*GpRCt{2 zn#*exNgT&NHPd;BiG&zM0}JCKsE5(t)@;t`7tc9uxv$4|`GYBnUa= zA22A0=Ahn%X$8T9k6EItdoel16#`j&#YuWB4?UUf-t^4WbWcwYc72A1>RiCoWHW4;lo?&`dZdg ze%-kcBrs*3(}zLEWjjcRYIF4j7+q`Fp1y(jTHm)*1wM&;z&e!%>Io>7N=%GBVq$F8vwmvY0-@YpC`byVG3jMlwTZu5;Lh_t zEWC`8}KB5n*Ij=K__`8Imb06UyDMy^jNj~CQO5}z|U#ZzUPJ8W|O5qK=-ehB^};0 z82d+Ioq(tVh#H-Mr~`^-s3*YI1b;&&Mi64G zVi#hps!b+Fc=qsmpa}C!BFQ1i0i$)iN0H?CB;Kob?@tqS>3?6J@=MzEwAl>fLlCXt zTn;-|++O(51Y>^-_|pX29jytrQWI=Xlr_QFj{!OXQ3nt;Iss7!5H+z+KuFVINXud8 zn_xkv{Ae8T?Qj^HU@MlxFkW?Cpf$lLG{GhLCq{B)mHpYnlk{=IcfZcJ!}jGqUaqmX z!#J@V=ZczO^Acchi@Wbp6V7sYW$7Cb^#mjmNiJL-;=s|Adpk@-tPFx)grW&{bgyCC zzMII#H$s_n_Kq( Date: Thu, 8 Aug 2019 16:51:20 +0200 Subject: [PATCH 5/5] [widgets][experimenteditor] Implement ExperimentEditor This version currently doesn't apply input/output checks. --- beat/editor/scripts/editor_cli.py | 1 + beat/editor/test/test_experimenteditor.py | 1164 +++++++++++++++++++- beat/editor/widgets/experimenteditor.py | 1183 ++++++++++++++++++++- 3 files changed, 2341 insertions(+), 7 deletions(-) diff --git a/beat/editor/scripts/editor_cli.py b/beat/editor/scripts/editor_cli.py index ee4afb7..9e46968 100644 --- a/beat/editor/scripts/editor_cli.py +++ b/beat/editor/scripts/editor_cli.py @@ -57,6 +57,7 @@ from ..backend.asset import AssetType from ..backend.asset import Asset from ..backend.eventfilters import MouseWheelFilter +from .. import resources # noqa Qt resources system, only import is needed from .. import version global logger diff --git a/beat/editor/test/test_experimenteditor.py b/beat/editor/test/test_experimenteditor.py index af24f96..62bf953 100644 --- a/beat/editor/test/test_experimenteditor.py +++ b/beat/editor/test/test_experimenteditor.py @@ -23,16 +23,1170 @@ # # ############################################################################### +import re +import copy +import pytest + +from PyQt5.QtCore import Qt +from PyQt5.QtCore import QStringListModel + +from PyQt5.QtWidgets import QComboBox +from PyQt5.QtWidgets import QCheckBox +from PyQt5.QtWidgets import QPushButton + +from beat.core.experiment import PROCESSOR_PREFIX +from beat.core.experiment import EVALUATOR_PREFIX + +from ..backend.asset import Asset +from ..backend.asset import AssetType +from ..backend.assetmodel import AssetModel + +from ..widgets.spinboxes import NumpySpinBox + +from ..widgets.experimenteditor import IOMapperDialog +from ..widgets.experimenteditor import DatasetEditor +from ..widgets.experimenteditor import DatasetModel +from ..widgets.experimenteditor import AlgorithmParametersEditor +from ..widgets.experimenteditor import ExecutionPropertiesEditor +from ..widgets.experimenteditor import BlockEditor +from ..widgets.experimenteditor import AnalyzerBlockEditor +from ..widgets.experimenteditor import LoopBlockEditor +from ..widgets.experimenteditor import GlobalParametersEditor +from ..widgets.experimenteditor import EnvironmentModel +from ..widgets.experimenteditor import FieldPresenceFilterProxyModel from ..widgets.experimenteditor import ExperimentEditor +from ..widgets.experimenteditor import typed_user_property + +from .conftest import prefix +from .conftest import sync_prefix + + +# ------------------------------------------------------------------------------ +# Constants + + +DEFAULT_ALGORITHM = "autonomous/parametrized/1" + + +# ------------------------------------------------------------------------------ +# Helpers + + +def get_experiment_declaration(prefix_path, experiment_name): + asset = Asset(prefix, AssetType.EXPERIMENT, experiment_name) + return asset.declaration + + +def get_algorithm_declaration(prefix_path, algorithm_name): + asset = Asset(prefix, AssetType.ALGORITHM, algorithm_name) + return asset.declaration + + +def get_parameters(prefix_path, algorithm_name): + return get_algorithm_declaration(prefix_path, algorithm_name)["parameters"] + + +def parameter_default_map(prefix_path, algorithm_name): + sync_prefix() + parameters = get_parameters(prefix_path, algorithm_name) + return {name: value["default"] for name, value in parameters.items()} + + +def parameter_range_map(prefix_path, algorithm_name): + sync_prefix() + parameters = get_parameters(prefix_path, algorithm_name) + return { + name: value["default"] for name, value in parameters.items() if "range" in value + } + + +def parameter_choice_map(prefix_path, algorithm_name): + sync_prefix() + parameters = get_parameters(prefix_path, algorithm_name) + return { + name: value["default"] + for name, value in parameters.items() + if "choice" in value + } + + +# ------------------------------------------------------------------------------ +# Fixtures + + +@pytest.fixture() +def algorithm_model(test_prefix): + algorithm_model = AssetModel() + algorithm_model.asset_type = AssetType.ALGORITHM + algorithm_model.prefix_path = test_prefix + return algorithm_model + + +@pytest.fixture() +def parametrized_algorithm(): + return DEFAULT_ALGORITHM + + +@pytest.fixture() +def test_experiment(): + return "user/user/two_loops/1/two_loops" + + +@pytest.fixture(params=parameter_default_map(prefix, DEFAULT_ALGORITHM)) +def parameter_name(request): + return request.param + + +@pytest.fixture(params=parameter_range_map(prefix, DEFAULT_ALGORITHM)) +def range_parameter_name(request): + return request.param + + +@pytest.fixture(params=parameter_choice_map(prefix, DEFAULT_ALGORITHM)) +def choice_parameter_name(request): + return request.param + + +# ------------------------------------------------------------------------------ +# Tests + + +class TestIOMapperDialog: + """Test that the dialog used for input/output mapping works as expected""" + + @staticmethod + def get_dialog_parameters(test_prefix, test_experiment, block_type): + experiment = Asset(test_prefix, AssetType.EXPERIMENT, test_experiment) + declaration = experiment.declaration + + block_data = next(iter(declaration[block_type].values())) + + algorithm = Asset(test_prefix, AssetType.ALGORITHM, block_data["algorithm"]) + return algorithm.declaration, block_data + + @pytest.fixture() + def algorithm_data(self, test_prefix, test_experiment): + return self.get_dialog_parameters(test_prefix, test_experiment, "blocks") + + @pytest.fixture() + def analyzer_data(self, test_prefix, test_experiment): + return self.get_dialog_parameters(test_prefix, test_experiment, "analyzers") + + def test_load_and_dump_algorithm(self, qtbot, algorithm_data): + algorithm, block_data = algorithm_data + dialog = IOMapperDialog(algorithm, block_data) + qtbot.addWidget(dialog) + io_mapping = dialog.ioMapping() + + assert "inputs" in io_mapping + assert "outputs" in io_mapping + for field in io_mapping: + assert block_data[field] == io_mapping[field] + + def test_load_and_dump_analyser(self, qtbot, analyzer_data): + analyzer, block_data = analyzer_data + dialog = IOMapperDialog(analyzer, block_data) + qtbot.addWidget(dialog) + io_mapping = dialog.ioMapping() + + assert "outputs" not in io_mapping + for field in io_mapping: + assert block_data[field] == io_mapping[field] + + +class TestDatasetEditor: + """Test that the dataset editor works as expected""" + + @pytest.fixture() + def datasets(self, test_prefix, test_experiment): + asset = Asset(test_prefix, AssetType.EXPERIMENT, test_experiment) + declaration = asset.declaration + return declaration.get("datasets") + + @pytest.fixture() + def original_dataset(self, datasets): + dataset = next(iter(datasets)) + return datasets[dataset] + + @pytest.fixture() + def other_dataset(self, datasets): + iterator = iter(datasets) + for i in range(0, 2): + dataset = next(iterator) + return datasets[dataset] + + @pytest.fixture() + def dataset_model(self, test_prefix): + dataset_model = DatasetModel() + dataset_model.setPrefixPath(test_prefix) + return dataset_model + + @pytest.fixture() + def editor(self, qtbot, test_prefix, datasets, dataset_model): + editor = DatasetEditor("test_block", test_prefix) + editor.setDatasetModel(dataset_model) + qtbot.addWidget(editor) + return editor + + def test_load_and_dump(self, qtbot, editor, original_dataset): + editor.load(original_dataset) + + assert editor.dump() == original_dataset + + def test_modification(self, qtbot, editor, original_dataset, other_dataset): + editor.load(original_dataset) + + assert editor.dump() == original_dataset + + new_entry = "{}/{}/{}".format( + other_dataset["database"], other_dataset["protocol"], other_dataset["set"] + ) + + with qtbot.waitSignal(editor.dataChanged): + editor.dataset_combobox.setCurrentText(new_entry) + + assert editor.dump() == other_dataset + + def test_reset(self, qtbot, editor, original_dataset, other_dataset): + editor.load(original_dataset) + + assert editor.dump() == original_dataset + + new_entry = "{}/{}/{}".format( + other_dataset["database"], other_dataset["protocol"], other_dataset["set"] + ) + + with qtbot.waitSignal(editor.dataChanged): + editor.dataset_combobox.setCurrentText(new_entry) + + assert editor.dump() == other_dataset + + with qtbot.waitSignal(editor.dataChanged): + qtbot.mouseClick(editor.reset_button, Qt.LeftButton) + + assert editor.dump() == original_dataset + + +class TestAlgorithmParametersEditor: + """Test that the algorithm parameters editor works as expected""" + + def test_load_and_dump(self, qtbot, test_prefix, parametrized_algorithm): + editor = AlgorithmParametersEditor(test_prefix) + qtbot.addWidget(editor) + assert editor.isEmpty() + + with qtbot.waitSignal(editor.parameterCountChanged) as blocker: + editor.setup(parametrized_algorithm) + assert blocker.args == [33] + + editor.load({}) + + assert editor.dump() == {} + + def test_edit(self, qtbot, test_prefix, parametrized_algorithm, parameter_name): + editor = AlgorithmParametersEditor(test_prefix) + qtbot.addWidget(editor) + editor.setup(parametrized_algorithm) + + parameter_values = parameter_default_map(test_prefix, parametrized_algorithm) + editor.load(parameter_values) + widget = editor.editorForLabel(parameter_name) + if isinstance(widget, NumpySpinBox): + with qtbot.waitSignal(editor.dataChanged): + widget.stepUp() + + elif isinstance(widget, QComboBox): + with qtbot.waitSignal(editor.dataChanged): + if widget.currentIndex() > 0: + new_index = 0 + else: + new_index = widget.count() - 1 + widget.setCurrentIndex(new_index) + + elif isinstance(widget, QCheckBox): + with qtbot.waitSignal(editor.dataChanged): + widget.setChecked(not widget.isChecked()) + + json_reference = {parameter_name: typed_user_property(widget)} + + assert editor.dump() == json_reference + + def test_range( + self, qtbot, test_prefix, parametrized_algorithm, range_parameter_name + ): + editor = AlgorithmParametersEditor(test_prefix) + qtbot.addWidget(editor) + editor.setup(parametrized_algorithm) + + parameters = get_parameters(test_prefix, parametrized_algorithm) + minimum, maximum = parameters[range_parameter_name]["range"] + + widget = editor.editorForLabel(range_parameter_name) + assert widget.minimum() == minimum + assert widget.maximum() == maximum + + def test_out_of_range( + self, qtbot, test_prefix, parametrized_algorithm, range_parameter_name + ): + editor = AlgorithmParametersEditor(test_prefix) + qtbot.addWidget(editor) + editor.setup(parametrized_algorithm) + editor.load({range_parameter_name: 1}) + + parameters = get_parameters(test_prefix, parametrized_algorithm) + minimum, maximum = parameters[range_parameter_name]["range"] + widget = editor.editorForLabel(range_parameter_name) + + with qtbot.waitSignal(editor.dataChanged): + widget.setValue(maximum + 1) + + json_reference = {range_parameter_name: maximum} + + assert editor.dump() == json_reference + + with qtbot.waitSignal(editor.dataChanged): + widget.setValue(minimum - 1) + + json_reference = {range_parameter_name: minimum} + + assert editor.dump() == json_reference + + def test_choice( + self, qtbot, test_prefix, parametrized_algorithm, choice_parameter_name + ): + editor = AlgorithmParametersEditor(test_prefix) + qtbot.addWidget(editor) + editor.setup(parametrized_algorithm) + + parameters = get_parameters(test_prefix, parametrized_algorithm) + parameter = parameters[choice_parameter_name] + choices = parameter["choice"] + + # Numerical choices are stored as text in the editor + choices = [str(choice) for choice in choices] + widget = editor.editorForLabel(choice_parameter_name) + widget.count() == len(choices) + for i in range(0, widget.count()): + assert widget.itemText(i) in choices + + +class TestFieldPresenceFilter: + """Test that the field presence filter works as expected""" + + @pytest.mark.parametrize("must_be_present", [True, False]) + @pytest.mark.parametrize("field_name", ["results", "type"]) + def test_field_presence(self, algorithm_model, field_name, must_be_present): + filter_model = FieldPresenceFilterProxyModel(field_name, must_be_present) + filter_model.setSourceModel(algorithm_model) + + assert filter_model.rowCount() < algorithm_model.rowCount() + + for i in range(filter_model.rowCount()): + algorithm_name = filter_model.index(i, 0).data() + declaration = get_algorithm_declaration( + algorithm_model.prefix_path, algorithm_name + ) + if must_be_present: + assert field_name in declaration + else: + assert field_name not in declaration + + @pytest.mark.parametrize("must_be_present", [True, False]) + @pytest.mark.parametrize( + "field_value", ["autonomous", "[autonomous|sequential]", ".*loop.*"] + ) + @pytest.mark.parametrize("field_name", ["type"]) + def test_field_value( + self, algorithm_model, field_name, must_be_present, field_value + ): + filter_model = FieldPresenceFilterProxyModel( + field_name, must_be_present, field_value + ) + filter_model.setSourceModel(algorithm_model) + + assert filter_model.rowCount() < algorithm_model.rowCount() + + for i in range(filter_model.rowCount()): + algorithm_name = filter_model.index(i, 0).data() + declaration = get_algorithm_declaration( + algorithm_model.prefix_path, algorithm_name + ) + if must_be_present: + value = declaration[field_name] + match = re.match(field_value, value) + assert match is not None + else: + if field_name in declaration: + value = declaration[field_name] + match = re.match(field_value, value) + assert match is None + + +class TestEnvironmentModel: + """Test that the environment model shows and return value as expected""" + + def test_setup(self, beat_context): + model = EnvironmentModel() + model.setContext(beat_context) + + assert model.rowCount() == 2 + + def test_visual_name(self, beat_context): + model = EnvironmentModel() + model.setContext(beat_context) + combobox = QComboBox() + combobox.setModel(model) + + for i in range(0, model.rowCount()): + environment = model.environment(model.index(i, 0)) + visual_name = "{name} ({version})".format(**environment) + assert combobox.itemText(i) == visual_name + + +class TestExecutionPropertiesEditor: + """Test that the AlgorithmEdior works as expected""" + + editor_klass = ExecutionPropertiesEditor + declaration_field = "blocks" + parameter_field = "parameters" + + @pytest.fixture() + def properties_editor(self, beat_context, test_prefix, algorithm_model): + environment_model = EnvironmentModel() + environment_model.setContext(beat_context) + + editor = self.editor_klass(test_prefix) + editor.setAlgorithmModel(algorithm_model) + editor.setEnvironmentModel(environment_model) + editor.setQueueModel(QStringListModel(["Test"])) + return editor + + def test_load_and_dump( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + first_algorithm = next(iter(algorithms)) + json_reference = algorithms[first_algorithm] + + properties_editor.load(json_reference) + assert properties_editor.dump() == json_reference + + def test_edit_environment( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + first_algorithm = next(iter(algorithms)) + json_reference = algorithms[first_algorithm] + + properties_editor.load(json_reference) + + environment_model = properties_editor.environmentModel() + environment = environment_model.environment(environment_model.index(1, 0)) + + with qtbot.waitSignal(properties_editor.dataChanged): + combobox = properties_editor.findChild(QComboBox, "environments") + combobox.setCurrentIndex(1) + + assert properties_editor.dump()["environment"] == environment + + def test_edit_algorithm( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + first_algorithm = next(iter(algorithms)) + json_reference = algorithms[first_algorithm] + + properties_editor.load(json_reference) + combobox = properties_editor.findChild(QComboBox, "algorithms") + + with qtbot.waitSignal(properties_editor.dataChanged): + next_index = combobox.currentIndex() + 1 + if next_index == combobox.count(): + next_index -= 2 + combobox.setCurrentIndex(next_index) + + assert properties_editor.dump()["algorithm"] == combobox.currentText() + + def test_edit_parameter( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + label = None + parameter_editor = None + for name, algorithm in algorithms.items(): + properties_editor.load(algorithm) + parameters_editors = properties_editor.findChildren( + AlgorithmParametersEditor + ) + for parameters_editor in parameters_editors: + if parameters_editor.parameterCount(): + label = parameters_editor.labelForRow(0) + parameter_editor = parameters_editor.editorForRow(0) + # proper way to break out of nested loops + # https://mail.python.org/pipermail/python-3000/2007-July/008663.html + return + + assert isinstance(parameter_editor, NumpySpinBox) + + with qtbot.waitSignal(properties_editor.dataChanged): + new_value = parameter_editor.value - 1 + parameter_editor.setValue(new_value) + + dump = properties_editor.dump() + assert dump[self.parameter_field] == {label: new_value} + + def test_edit_parameter_going_back_to_default_value( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + parameter_editor = None + for name, algorithm in algorithms.items(): + properties_editor.load(algorithm) + parameters_editors = properties_editor.findChildren( + AlgorithmParametersEditor + ) + for parameters_editor in parameters_editors: + if parameters_editor.parameterCount(): + label = parameters_editor.labelForRow(0) + parameter_editor = parameters_editor.editorForRow(0) + break + + assert isinstance(parameter_editor, NumpySpinBox) + + with qtbot.waitSignal(properties_editor.dataChanged): + new_value = parameter_editor.value - 1 + parameter_editor.setValue(new_value) + + with qtbot.waitSignal(properties_editor.dataChanged): + new_value = parameter_editor.value + 1 + parameter_editor.setValue(new_value) + + dump = properties_editor.dump() + assert dump[self.parameter_field] == {label: new_value} + + def test_edit_io_mapping( + self, + qtbot, + monkeypatch, + properties_editor, + test_prefix, + test_experiment, + algorithm_model, + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + + algorithms = experiment_declaration[self.declaration_field] + first_algorithm = next(iter(algorithms)) + json_reference = algorithms[first_algorithm] + + properties_editor.load(json_reference) + + io_mapping_answer = {"inputs": json_reference["inputs"]} + + for key in io_mapping_answer["inputs"]: + io_mapping_answer["inputs"] = "changed" + + if "outputs" in json_reference: + io_mapping_answer["outputs"] = json_reference["outputs"] + for key in io_mapping_answer["outputs"]: + io_mapping_answer["outputs"] = "changed" + + monkeypatch.setattr( + IOMapperDialog, "getIOMapping", lambda *args: (True, io_mapping_answer) + ) + + remap_button = properties_editor.findChild(QPushButton, "remap") + with qtbot.waitSignal(properties_editor.dataChanged): + qtbot.mouseClick(remap_button, Qt.LeftButton) + + assert properties_editor.dump()["inputs"] == io_mapping_answer["inputs"] + if "outputs" in json_reference: + assert properties_editor.dump()["outputs"] == io_mapping_answer["outputs"] + + +class TestBlockEditor(TestExecutionPropertiesEditor): + """Test that the editor for blocks works correctly""" + + editor_klass = BlockEditor + declaration_field = "blocks" + + @pytest.fixture() + def properties_editor(self, beat_context, test_prefix, algorithm_model): + environment_model = EnvironmentModel() + environment_model.setContext(beat_context) + + editor = self.editor_klass("block_name", test_prefix) + editor.setAlgorithmModel(algorithm_model) + editor.setEnvironmentModel(environment_model) + editor.setQueueModel(QStringListModel(["Test"])) + return editor + + +class TestAnalyzerBlockEditor(TestExecutionPropertiesEditor): + """Test that the editor for analyzer blocks works correctly""" + + editor_klass = AnalyzerBlockEditor + declaration_field = "analyzers" + + @pytest.fixture() + def properties_editor(self, beat_context, test_prefix, algorithm_model): + environment_model = EnvironmentModel() + environment_model.setContext(beat_context) + + editor = self.editor_klass("block_name", test_prefix) + editor.setAlgorithmModel(algorithm_model) + editor.setEnvironmentModel(environment_model) + editor.setQueueModel(QStringListModel(["Test"])) + return editor + + @pytest.mark.skip(reason="Analyzers don't have properties") + def test_edit_parameter( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + pass + + @pytest.mark.skip(reason="Analyzers don't have properties") + def test_edit_parameter_going_back_to_default_value( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + pass + + +class TestLoopBlockEditor(TestExecutionPropertiesEditor): + """Test that the editor for the loop blocks works correctly""" + + editor_klass = LoopBlockEditor + declaration_field = "loops" + + @pytest.fixture() + def properties_editor(self, beat_context, test_prefix, algorithm_model): + environment_model = EnvironmentModel() + environment_model.setContext(beat_context) + + editor = self.editor_klass("block_name", test_prefix) + editor.setAlgorithmModel(algorithm_model) + editor.setEnvironmentModel(environment_model) + editor.setQueueModel(QStringListModel(["Test"])) + return editor + + def test_edit_environment( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + first_algorithm = next(iter(algorithms)) + json_reference = algorithms[first_algorithm] + + properties_editor.load(json_reference) + + environment_model = properties_editor.environmentModel() + environment = environment_model.environment(environment_model.index(1, 0)) + + for editor in [ + properties_editor.processor_properties_editor, + properties_editor.evaluator_properties_editor, + ]: + with qtbot.waitSignal(properties_editor.dataChanged): + combobox = editor.findChild(QComboBox, "environments") + combobox.setCurrentIndex(1) + + assert properties_editor.dump()[PROCESSOR_PREFIX + "environment"] == environment + assert properties_editor.dump()[EVALUATOR_PREFIX + "environment"] == environment + + def test_edit_algorithm( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + first_algorithm = next(iter(algorithms)) + json_reference = algorithms[first_algorithm] + + properties_editor.load(json_reference) + processor_combobox = properties_editor.processor_properties_editor.findChild( + QComboBox, "algorithms" + ) + evaluator_combobox = properties_editor.evaluator_properties_editor.findChild( + QComboBox, "algorithms" + ) + + for combobox in [processor_combobox, evaluator_combobox]: + with qtbot.waitSignal(properties_editor.dataChanged): + next_index = combobox.currentIndex() + 1 + if next_index == combobox.count(): + next_index -= 2 + combobox.setCurrentIndex(next_index) + + assert ( + properties_editor.dump()[PROCESSOR_PREFIX + "algorithm"] + == processor_combobox.currentText() + ) + assert ( + properties_editor.dump()[EVALUATOR_PREFIX + "algorithm"] + == evaluator_combobox.currentText() + ) + + def test_edit_parameter( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + + tested = {} + + for name, algorithm in algorithms.items(): + properties_editor.load(algorithm) + + for field_prefix, sub_properties_editor in [ + (PROCESSOR_PREFIX, properties_editor.processor_properties_editor), + (EVALUATOR_PREFIX, properties_editor.evaluator_properties_editor), + ]: + parameters_editors = sub_properties_editor.findChildren( + AlgorithmParametersEditor + ) + for parameters_editor in parameters_editors: + if parameters_editor.parameterCount(): + label = parameters_editor.labelForRow(0) + parameter_editor = parameters_editor.editorForRow(0) + + assert isinstance(parameter_editor, NumpySpinBox) + + with qtbot.waitSignal(properties_editor.dataChanged): + new_value = parameter_editor.value - 1 + parameter_editor.setValue(new_value) + + assert properties_editor.dump()[ + field_prefix + self.parameter_field + ] == {label: new_value} + tested.update({field_prefix: True}) + break + + assert tested.get(PROCESSOR_PREFIX, False) + assert tested.get(EVALUATOR_PREFIX, False) + + def test_edit_parameter_going_back_to_default_value( + self, qtbot, properties_editor, test_prefix, test_experiment, algorithm_model + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + algorithms = experiment_declaration[self.declaration_field] + + tested = {} + + for name, algorithm in algorithms.items(): + properties_editor.load(algorithm) + + for field_prefix, sub_properties_editor in [ + (PROCESSOR_PREFIX, properties_editor.processor_properties_editor), + (EVALUATOR_PREFIX, properties_editor.evaluator_properties_editor), + ]: + parameter_field = field_prefix + self.parameter_field + if parameter_field not in algorithm: + continue + + parameters_editors = sub_properties_editor.findChildren( + AlgorithmParametersEditor + ) + for parameters_editor in parameters_editors: + if parameters_editor.parameterCount(): + label = parameters_editor.labelForRow(0) + parameter_editor = parameters_editor.editorForRow(0) + + assert isinstance(parameter_editor, NumpySpinBox) + + with qtbot.waitSignal(properties_editor.dataChanged): + new_value = parameter_editor.value - 1 + parameter_editor.setValue(new_value) + + with qtbot.waitSignal(properties_editor.dataChanged): + new_value = parameter_editor.value + 1 + parameter_editor.setValue(new_value) + + assert properties_editor.dump()[parameter_field] == { + label: new_value + } + tested.update({field_prefix: True}) + break + + assert tested.get(PROCESSOR_PREFIX, False) + assert tested.get(EVALUATOR_PREFIX, False) + + def test_edit_io_mapping( + self, + qtbot, + monkeypatch, + properties_editor, + test_prefix, + test_experiment, + algorithm_model, + ): + qtbot.addWidget(properties_editor) + + experiment_declaration = get_experiment_declaration( + test_prefix, test_experiment + ) + + algorithms = experiment_declaration[self.declaration_field] + first_algorithm = next(iter(algorithms)) + json_reference = algorithms[first_algorithm] + + properties_editor.load(json_reference) + + for field_prefix, sub_properties_editor in [ + (PROCESSOR_PREFIX, properties_editor.processor_properties_editor), + (EVALUATOR_PREFIX, properties_editor.evaluator_properties_editor), + ]: + prefixed_inputs = field_prefix + "inputs" + prefixed_outputs = field_prefix + "outputs" + + io_mapping_answer = {"inputs": json_reference[prefixed_inputs]} + + for key in io_mapping_answer["inputs"]: + io_mapping_answer["inputs"] = "changed" + + if prefixed_outputs in json_reference: + io_mapping_answer["outputs"] = json_reference[prefixed_outputs] + for key in io_mapping_answer["outputs"]: + io_mapping_answer["outputs"] = "changed" + + monkeypatch.setattr( + IOMapperDialog, "getIOMapping", lambda *args: (True, io_mapping_answer) + ) + + remap_button = sub_properties_editor.findChild(QPushButton, "remap") + with qtbot.waitSignal(sub_properties_editor.dataChanged): + qtbot.mouseClick(remap_button, Qt.LeftButton) + + assert ( + properties_editor.dump()[prefixed_inputs] == io_mapping_answer["inputs"] + ) + + if prefixed_outputs in json_reference: + assert ( + properties_editor.dump()[prefixed_outputs] + == io_mapping_answer["outputs"] + ) + + +EXP_GLOBALS = { + "queue": "queue", + "environment": {"name": "Python 2.7", "version": "1.3.0"}, +} + +EXP_GLOBALS_PARAMETERS = { + "v1/integers_add/1": {"offset": 1}, + "user/db_input_loop_evaluator/1": {"threshold": 9}, +} + + +class TestGlobalParametersEditor: + """Test that the global parameters editor works correctly""" + + @pytest.fixture() + def exp_globals(self): + return copy.deepcopy(EXP_GLOBALS) + + @pytest.fixture() + def exp_globals_parameters(self): + return copy.deepcopy(EXP_GLOBALS_PARAMETERS) + + @pytest.fixture() + def gpe_editor(self, beat_context, test_prefix): + editor = GlobalParametersEditor(test_prefix) + environment_model = EnvironmentModel() + environment_model.setContext(beat_context) + editor.setEnvironmentModel(environment_model) + editor.setQueueModel(QStringListModel(["Test", "Test2"])) + return editor + + @pytest.mark.parametrize( + "parameters", + [{}, EXP_GLOBALS_PARAMETERS], + ids=["No parameters", "With Parameters"], + ) + def test_load_and_dump(self, qtbot, gpe_editor, exp_globals, parameters): + exp_globals.update(parameters) + gpe_editor.setup(set(parameters.keys())) + gpe_editor.load(exp_globals) + + assert gpe_editor.dump() == exp_globals + + def test_add_algorithm( + self, qtbot, gpe_editor, exp_globals, exp_globals_parameters + ): + gpe_editor.load(exp_globals) + + assert gpe_editor.dump() == exp_globals + + gpe_editor.setup(set(exp_globals_parameters.keys())) + exp_globals.update(exp_globals_parameters) + assert gpe_editor.dump() == exp_globals + + def test_remove_algorithm( + self, qtbot, gpe_editor, exp_globals, exp_globals_parameters + ): + original_globals = copy.deepcopy(exp_globals) + + exp_globals.update(exp_globals_parameters) + gpe_editor.setup(set(exp_globals_parameters.keys())) + gpe_editor.load(exp_globals) + + assert gpe_editor.dump() == exp_globals + + gpe_editor.setup(set()) + assert gpe_editor.dump() == original_globals + + def test_change_environment(self, qtbot, gpe_editor, exp_globals): + gpe_editor.load(exp_globals) + with qtbot.waitSignal(gpe_editor.dataChanged): + gpe_editor.environment_combobox.setCurrentIndex(1) + assert gpe_editor.dump() != exp_globals + + def test_change_queue(self, qtbot, gpe_editor, exp_globals): + gpe_editor.load(exp_globals) + with qtbot.waitSignal(gpe_editor.dataChanged): + gpe_editor.queue_combobox.setCurrentIndex(1) + assert gpe_editor.dump() != exp_globals + + +def get_valid_experiments(test_prefix): + model = AssetModel() + model.asset_type = AssetType.EXPERIMENT + model.prefix_path = test_prefix + model.setLatestOnlyEnabled(False) + return [ + experiment for experiment in model.stringList() if "errors" not in experiment + ] class TestExperimentEditor: - """Test that the mock editor works correctly""" + """Test that the experiment editor works correctly""" + + @pytest.fixture() + def test_experiment(self): + return "user/user/single/1/single_add" + + @pytest.fixture() + def experiment_declaration(self, test_prefix, test_experiment): + return get_experiment_declaration(test_prefix, test_experiment) - def test_load_and_dump(self, qtbot): - reference_json = {"description": "test"} + @pytest.fixture() + def experiment_editor(self, qtbot, beat_context, experiment_declaration): editor = ExperimentEditor() + editor.set_context(beat_context) + editor.load_json(experiment_declaration) + qtbot.addWidget(editor) + return editor + + @pytest.mark.parametrize("experiment", get_valid_experiments(prefix)) + def test_load_and_dump(self, qtbot, beat_context, test_prefix, experiment): + experiment_declaration = get_experiment_declaration(test_prefix, experiment) + editor = ExperimentEditor() + editor.set_context(beat_context) + editor.load_json(experiment_declaration) + qtbot.addWidget(editor) + + assert editor.dump_json() == experiment_declaration + + def test_change_dataset(self, qtbot, experiment_editor, experiment_declaration): + dataset_editor = experiment_editor.datasets_widget.widget_list[0] + dataset_combobox = dataset_editor.dataset_combobox + with qtbot.waitSignal(experiment_editor.dataChanged): + dataset_combobox.setCurrentIndex(dataset_combobox.currentIndex() + 1) + + dump = experiment_editor.dump_json() + assert dump != experiment_declaration + assert dump["datasets"]["set"]["set"] == dataset_editor.currentSet() + + def test_change_one_algorithm_parameter( + self, qtbot, experiment_editor, experiment_declaration + ): + block_name = None + parameter_editor = None + for widget in experiment_editor.blocks_widget.widget_list: + parameters_editor = widget.properties_editor.parameters_editor + if parameters_editor.parameterCount(): + block_name = widget.block_name + parameter_editor = parameters_editor.editorForRow(0) + break + + assert isinstance(parameter_editor, NumpySpinBox) + + with qtbot.waitSignal(experiment_editor.dataChanged): + new_value = parameter_editor.value - 1 + parameter_editor.setValue(new_value) + + dump = experiment_editor.dump_json() + assert dump != experiment_declaration + assert "parameters" in dump["blocks"][block_name] + + @pytest.mark.parametrize( + ["widget_attribute", "json_entry"], + [("blocks_widget", "blocks"), ("analyzers_widget", "analyzers")], + ids=["Blocks", "Analyzers"], + ) + def test_change_algorithm_in_blocks( + self, + qtbot, + experiment_editor, + experiment_declaration, + widget_attribute, + json_entry, + ): + list_widget = getattr(experiment_editor, widget_attribute) + block_editor = list_widget.widget_list[0] + combobox = block_editor.properties_editor.algorithm_combobox + with qtbot.waitSignal(experiment_editor.dataChanged): + combobox.setCurrentIndex(combobox.currentIndex() + 1) + + dump = experiment_editor.dump_json() + assert dump != experiment_declaration + assert ( + dump[json_entry][block_editor.block_name]["algorithm"] + == combobox.currentText() + ) + + @pytest.mark.parametrize( + ["widget_attribute", "json_entry"], + [("blocks_widget", "blocks"), ("analyzers_widget", "analyzers")], + ids=["Blocks", "Analyzers"], + ) + def test_change_environment_in_blocks( + self, + qtbot, + experiment_editor, + experiment_declaration, + widget_attribute, + json_entry, + ): + list_widget = getattr(experiment_editor, widget_attribute) + block_editor = list_widget.widget_list[0] + properties_editor = block_editor.properties_editor + combobox = properties_editor.environment_combobox + with qtbot.waitSignal(experiment_editor.dataChanged): + combobox.setCurrentIndex(1) + + dump = experiment_editor.dump_json() + assert dump != experiment_declaration + + model = combobox.model() + assert dump[json_entry][block_editor.block_name][ + "environment" + ] == model.environment(model.index(1, 0)) + + def test_change_environment_in_globals( + self, qtbot, experiment_editor, experiment_declaration + ): + gpe_editor = experiment_editor.globalparameters_widget + with qtbot.waitSignal(experiment_editor.dataChanged): + gpe_editor.environment_combobox.setCurrentIndex(1) + + dump = experiment_editor.dump_json() + + assert dump != experiment_declaration + model = gpe_editor.environment_combobox.model() + assert dump["globals"]["environment"] == model.environment(model.index(1, 0)) + + def test_no_parameter_in_algorithm_if_same_value_in_globals( + self, qtbot, experiment_editor, experiment_declaration + ): + algorithm_name = None + block_name = None + parameter_editor = None + for block_editor in experiment_editor.blocks_widget.widget_list: + properties_editor = block_editor.properties_editor + if properties_editor.parameters_editor.parameterCount(): + block_name = block_editor.block_name + algorithm_name = properties_editor.parameters_editor.algorithm_name + parameter_editor = properties_editor.parameters_editor.editorForRow(0) + break + + assert isinstance(parameter_editor, NumpySpinBox) + + with qtbot.waitSignal(experiment_editor.dataChanged): + new_value = parameter_editor.value - 1 + parameter_editor.setValue(new_value) + + dump = experiment_editor.dump_json() + assert dump != experiment_declaration + assert "parameters" in dump["blocks"][block_name] + + gpe_editor = experiment_editor.globalparameters_widget + gpe_parameter_editor = None + + for widget in gpe_editor.parameters_editor_listwidget.widget_list: + if widget.editor.algorithm_name == algorithm_name: + gpe_parameter_editor = widget.editor.editorForRow(0) + break + + assert isinstance(gpe_parameter_editor, NumpySpinBox) + gpe_parameter_editor.setValue(parameter_editor.value) + assert gpe_parameter_editor.value == parameter_editor.value + + dump = experiment_editor.dump_json() + assert dump != experiment_declaration + assert "parameters" not in dump["blocks"][block_name] + + def test_no_environment_in_algorithm_if_same_value_in_globals( + self, qtbot, experiment_editor, experiment_declaration + ): + block_editor = experiment_editor.blocks_widget.widget_list[0] + properties_editor = block_editor.properties_editor + with qtbot.waitSignal(experiment_editor.dataChanged): + properties_editor.environment_combobox.setCurrentIndex(1) + + dump = experiment_editor.dump_json() + + assert dump != experiment_declaration + assert "environment" in dump["blocks"][block_editor.block_name] - editor.load_json(reference_json) + gpe_editor = experiment_editor.globalparameters_widget + with qtbot.waitSignal(experiment_editor.dataChanged): + gpe_editor.environment_combobox.setCurrentIndex(1) - assert editor.dump_json() == reference_json + dump = experiment_editor.dump_json() + assert dump != experiment_declaration + assert "environment" not in dump["blocks"][block_editor.block_name] diff --git a/beat/editor/widgets/experimenteditor.py b/beat/editor/widgets/experimenteditor.py index 13595b6..cfeeec4 100644 --- a/beat/editor/widgets/experimenteditor.py +++ b/beat/editor/widgets/experimenteditor.py @@ -23,12 +23,1014 @@ # # ############################################################################### +import re +import copy +import simplejson as json +import numpy as np + +from PyQt5.QtCore import pyqtProperty +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import QStringListModel +from PyQt5.QtCore import QSortFilterProxyModel + +from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QStandardItem +from PyQt5.QtGui import QStandardItemModel + +from PyQt5.QtWidgets import QCheckBox +from PyQt5.QtWidgets import QComboBox +from PyQt5.QtWidgets import QDialogButtonBox +from PyQt5.QtWidgets import QDialog +from PyQt5.QtWidgets import QFormLayout +from PyQt5.QtWidgets import QGroupBox +from PyQt5.QtWidgets import QHBoxLayout +from PyQt5.QtWidgets import QLabel +from PyQt5.QtWidgets import QLineEdit +from PyQt5.QtWidgets import QPushButton +from PyQt5.QtWidgets import QStyle +from PyQt5.QtWidgets import QTabWidget +from PyQt5.QtWidgets import QVBoxLayout +from PyQt5.QtWidgets import QWidget + +from beat.core.experiment import PROCESSOR_PREFIX +from beat.core.experiment import EVALUATOR_PREFIX + +from ..backend.asset import Asset from ..backend.asset import AssetType +from ..backend.assetmodel import AssetModel + from ..decorators import frozen +from ..utils import is_Qt_equal_or_higher + +from .scrollwidget import ScrollWidget +from .scrollwidget import EditorListWidget + +from .spinboxes import NumpySpinBox + from .editor import AbstractAssetEditor +PARAMETER_TYPE_KEY = "parameter_type" +DEFAULT_VALUE_KEY = "default_value" +EDITED_KEY = "edited" + + +# ------------------------------------------------------------------------------ +# Helper methods + + +def typed_user_property(widget): + """Returns the user property value properly typed + + :param widget QWidget: widget from which the property value must be retrieved + """ + + user_property = widget.metaObject().userProperty() + parameter_type = widget.property(PARAMETER_TYPE_KEY) + return np.array([user_property.read(widget)]).astype(parameter_type)[0] + + +def write_user_property(widget, value): + """Write the widget user property value + + :param widget QWidget: widget to which the property value must be written + :param value: value to write to the user property + """ + + user_property = widget.metaObject().userProperty() + user_property.write(widget, value) + + +# ------------------------------------------------------------------------------ +# Helper classes + + +class FieldPresenceFilterProxyModel(QSortFilterProxyModel): + """Filter proxy model showing asset filtered based on their content""" + + def __init__(self, field, must_have, value=None, parent=None): + """ + :param field str: field to search + :param must_have bool: whether the field must be in the declaration + """ + + super().__init__(parent=parent) + + self.setFieldFilter(field, must_have, value) + + def setFieldFilter(self, field, must_have, value=None): + self.field = field + self.must_have = must_have + self.value = value + self.prog = re.compile(value) if value is not None else None + + def filterAcceptsRow(self, source_row, source_parent): + """Filter assets based on whether the field configured must or must not + be found in the declaration. + """ + + asset_model = self.sourceModel() + index = asset_model.index(source_row, 0, source_parent) + path = asset_model.json_path(index.data()) + + with open(path, "rt") as json_file: + try: + json_data = json.load(json_file) + except json.JSONDecodeError: + return False + else: + has_field = self.field in json_data + if has_field and self.value is not None: + match = self.prog.match(json_data[self.field]) + if self.must_have: + return match is not None + else: + return match is None + return has_field == self.must_have + + +class EnvironmentModel(QStandardItemModel): + """Model wrapping the processing environment available""" + + def __init__(self, parent=None): + super().__init__(3, 0, parent) + + self.context = None + + @pyqtSlot() + def refreshContent(self): + self.clear() + if self.context is not None: + with open(self.context.meta["environments"], "rt") as file_: + json_data = json.load(file_) + for docker_name, data in json_data.items(): + if "databases" not in data: + visual_name = "{name} ({version})".format(**data) + self.appendRow( + [ + QStandardItem(visual_name), + QStandardItem(data["name"]), + QStandardItem(data["version"]), + ] + ) + + def setContext(self, context): + self.context = context + self.refreshContent() + + def environment(self, index): + """Returns the (name, version) tuple representing an environment + + :param index QModelIndex: index from the model + """ + + if is_Qt_equal_or_higher("5.11"): + name = index.siblingAtColumn(1) + version = index.siblingAtColumn(2) + else: + name = index.sibling(index.row(), 1) + version = index.sibling(index.row(), 2) + + name = self.data(name) + version = self.data(version) + return {"name": name, "version": version} + + +class ContainerWidget(ScrollWidget): + """Container widget to show block editors""" + + def dump(self): + return {widget.block_name: widget.dump() for widget in self.widget_list} + + +class AbstractBaseEditor(QWidget): + + dataChanged = pyqtSignal() + prefixPathChanged = pyqtSignal(str) + + def __init__(self, prefix_path, parent=None): + super().__init__(parent) + self.prefix_path = prefix_path + + @pyqtSlot(str) + def setPrefixPath(self, prefix_path): + if self.prefix_path == prefix_path: + return + + self.prefix_path = prefix_path + self.prefixPathChanged.emit(self.prefix_path) + + def load(self, json_object): + raise NotImplementedError + + def dump(self): + raise NotImplementedError + + +class DatasetModel(QStringListModel): + def __init__(self, parent=None): + super().__init__(parent) + + def update(self): + asset_model = AssetModel() + asset_model.asset_type = AssetType.DATABASE + asset_model.prefix_path = self.prefix_path + asset_model.setLatestOnlyEnabled(False) + + available_databases = asset_model.stringList() + available_set_names = [] + + for database in available_databases: + db = AssetType.DATABASE.klass(self.prefix_path, database) + if not db.valid: + continue + for protocol in db.protocol_names: + for set_name in db.set_names(protocol): + available_set_names.append( + "{}/{}/{}".format(database, protocol, set_name) + ) + self.setStringList(available_set_names) + + def setPrefixPath(self, prefix_path): + self.prefix_path = prefix_path + self.update() + + +# ------------------------------------------------------------------------------ +# Editors + + +class IOMapperDialog(QDialog): + """Dialog that allows to remap inputs and outputs of an algorithm to fit on + a block of the toolchain + """ + + def __init__(self, algorithm, block_data, parent=None): + super().__init__(parent) + alg_inputs = [] + alg_outputs = [] + for group in algorithm["groups"]: + alg_inputs += group["inputs"].keys() + outputs = group.get("outputs") + if outputs: + alg_outputs += outputs.keys() + + self.inputs_layout = QFormLayout() + for alg_input, input_ in block_data["inputs"].items(): + combobox = QComboBox() + combobox.addItems(alg_inputs) + combobox.setCurrentText(alg_input) + self.inputs_layout.addRow(input_, combobox) + + inputs_groupbox = QGroupBox(self.tr("Inputs")) + inputs_groupbox.setLayout(self.inputs_layout) + groupboxes_layout = QHBoxLayout() + groupboxes_layout.addWidget(inputs_groupbox) + + self.outputs_layout = QFormLayout() + + outputs = block_data.get("outputs") + if outputs: + for alg_output, output in block_data["outputs"].items(): + combobox = QComboBox() + combobox.addItems(alg_outputs) + combobox.setCurrentText(alg_output) + self.outputs_layout.addRow(output, combobox) + + outputs_groupbox = QGroupBox(self.tr("Outputs")) + outputs_groupbox.setLayout(self.outputs_layout) + groupboxes_layout.addWidget(outputs_groupbox) + + buttonbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + layout = QVBoxLayout(self) + layout.addLayout(groupboxes_layout) + layout.addWidget(buttonbox) + + buttonbox.accepted.connect(self.accept) + buttonbox.rejected.connect(self.reject) + + def ioMapping(self): + data = {"inputs": {}} + + for i in range(self.inputs_layout.count() - 1): + label = self.inputs_layout.itemAt(i, QFormLayout.LabelRole).widget() + combobox = self.inputs_layout.itemAt(i, QFormLayout.FieldRole).widget() + data["inputs"][combobox.currentText()] = label.text() + + for i in range(self.outputs_layout.count() - 1): + if "outputs" not in data: + data["outputs"] = {} + label = self.outputs_layout.itemAt(i, QFormLayout.LabelRole).widget() + combobox = self.outputs_layout.itemAt(i, QFormLayout.FieldRole).widget() + data["outputs"][combobox.currentText()] = label.text() + + return data + + @staticmethod + def getIOMapping(prefix, block_data): + algorithm = Asset(prefix, AssetType.ALGORITHM, block_data["algorithm"]) + + dialog = IOMapperDialog(algorithm.declaration, block_data) + status = dialog.exec_() + if status == QDialog.Rejected: + return False, None + + return True, dialog.ioMapping() + + +class DatasetEditor(AbstractBaseEditor): + """Widget allowing the setup of the various datasets used""" + + def __init__(self, block_name, prefix_path, parent=None): + super().__init__(prefix_path, parent) + + self.json_object = {} + self.block_name = block_name + self.dataset_combobox = QComboBox() + self.reset_button = QPushButton( + self.style().standardIcon(QStyle.SP_DialogResetButton), "" + ) + self.reset_button.setToolTip(self.tr("Reset values")) + + row_layout = QHBoxLayout() + row_layout.addWidget(self.dataset_combobox, 1) + row_layout.addWidget(self.reset_button) + + layout = QFormLayout(self) + layout.addRow(self.block_name, row_layout) + + self.dataset_combobox.currentTextChanged.connect(self.dataChanged) + self.reset_button.clicked.connect(self.reset) + + def reset(self): + self.dataset_combobox.setCurrentText( + "{}/{}/{}".format( + self.json_object["database"], + self.json_object["protocol"], + self.json_object["set"], + ) + ) + + def setDatasetModel(self, dataset_model): + self.dataset_combobox.setModel(dataset_model) + + def currentSet(self): + _, _, _, set_ = self.dataset_combobox.currentText().split("/") + return set_ + + def load(self, json_object): + self.json_object = copy.deepcopy(json_object) + self.reset() + + def dump(self): + database, db_version, protocol, set_ = self.dataset_combobox.currentText().split( + "/" + ) + return { + "database": "{}/{}".format(database, db_version), + "protocol": protocol, + "set": set_, + } + + +class AlgorithmParametersEditor(AbstractBaseEditor): + + dataChanged = pyqtSignal() + parameterCountChanged = pyqtSignal(int) + + def __init__(self, prefix_path, parent=None): + super().__init__(prefix_path, parent) + + self.algorithm_name = None + self.json_data = {} + self.edited = {} + self.dump_edited_only = True + self.form_layout = QFormLayout(self) + + def setDumpEditOnlyEnabled(self, enabled): + """Sets whether the dump will contain all or only the modified fields""" + + self.dump_edited_only = enabled + + def isEmpty(self): + """Returns whether this editor is empty""" + + return self.form_layout.rowCount() == 0 + + def parameterCount(self): + """Returns the number of parameters to edit""" + + return self.form_layout.rowCount() + + def editorForRow(self, row): + """Returns the editor widget matching the give row""" + + if row < 0 or row >= self.form_layout.rowCount(): + return None + + return self.form_layout.itemAt(row, QFormLayout.FieldRole).widget() + + def labelForRow(self, row): + """Returns the label widget matching the give row""" + + if row < 0 or row >= self.form_layout.rowCount(): + return None + + return self.form_layout.itemAt(row, QFormLayout.LabelRole).widget().text() + + def editorForLabel(self, label): + """Returns the editor matching the given label""" + + for i in range(self.form_layout.rowCount()): + if self.labelForRow(i) == label: + return self.editorForRow(i) + + def setup(self, algorithm_name): + """Setup the editor for the give algorithm""" + + if self.algorithm_name == algorithm_name: + return + + self.algorithm_name = algorithm_name + old_count = self.parameterCount() + + while self.form_layout.rowCount(): + self.form_layout.removeRow(0) + + algorithm = AssetType.ALGORITHM.klass(self.prefix_path, self.algorithm_name) + parameters = algorithm.parameters + if parameters is not None: + for name, info in parameters.items(): + type_ = np.dtype(info["type"]) + default = info.get("default") + + if np.issubdtype(type_, np.number): + if "choice" in info: + editor = QComboBox() + signal = editor.currentTextChanged + editor.addItems([str(value) for value in info["choice"]]) + editor.setCurrentText(str(default)) + else: + editor = NumpySpinBox(type_.type) + signal = editor.valueChanged + if "range" in info: + range_ = info["range"] + editor.setRange(range_[0], range_[1]) + editor.setValue(default) + elif np.issubdtype(type_, np.dtype(str).type): + if "choice" in info: + editor = QComboBox() + signal = editor.currentTextChanged + editor.addItems(info["choice"]) + editor.setCurrentText(str(default)) + else: + editor = QLineEdit() + signal = editor.textChanged + editor.setText(default) + + elif np.issubdtype(type_, np.dtype(bool).type): + editor = QCheckBox() + signal = editor.toggled + editor.setChecked(bool(default)) + else: + raise RuntimeError("Unsupported type: {}".format(type_)) + + signal.connect(self.dataChanged) + signal.connect(lambda: self.sender().setProperty(EDITED_KEY, True)) + editor.setProperty(PARAMETER_TYPE_KEY, type_) + editor.setProperty(DEFAULT_VALUE_KEY, default) + editor.setProperty(EDITED_KEY, False) + + self.form_layout.addRow(name, editor) + + new_count = self.parameterCount() + if new_count != old_count: + self.parameterCountChanged.emit(new_count) + + def reset(self): + """Reset the state of the editor all default values""" + + for i in range(self.form_layout.rowCount()): + widget = self.form_layout.itemAt(i, QFormLayout.FieldRole).widget() + widget.setProperty(EDITED_KEY, False) + default = widget.property(DEFAULT_VALUE_KEY) + write_user_property(widget, default) + + def load(self, json_data): + """Setup the content of the editor with the given JSON data""" + + self.json_data = copy.deepcopy(json_data) + + for name, value in json_data.items(): + for i in range(self.form_layout.rowCount()): + label = self.form_layout.itemAt(i, QFormLayout.LabelRole).widget() + if label.text() == name: + widget = self.form_layout.itemAt(i, QFormLayout.FieldRole).widget() + write_user_property(widget, value) + break + + def dump(self): + """Dump only the parameters that have changed unless dump edited only + is set. + """ + + data = {} + for i in range(0, self.form_layout.rowCount()): + label = self.form_layout.itemAt(i, QFormLayout.LabelRole).widget() + name = label.text() + + widget = self.form_layout.itemAt(i, QFormLayout.FieldRole).widget() + value = typed_user_property(widget) + if self.dump_edited_only: + default_value = widget.property(DEFAULT_VALUE_KEY) + if widget.property(EDITED_KEY) and value != default_value: + data[name] = value + else: + data[name] = value + return data + + +class ExecutionPropertiesEditor(AbstractBaseEditor): + """Widget showing the execution parameters related to an execution unit""" + + algorithmChanged = pyqtSignal(str) + + def __init__(self, prefix_path, parent=None): + super().__init__(prefix_path, parent) + + self.json_object = {} + self.io_mapping = {} + self.environment_changed = False + self.queue_changed = False + self._queue_enabled = True + self.parameter_item = None + self.algorithm_combobox = QComboBox() + self.algorithm_combobox.setObjectName("algorithms") + self.environment_combobox = QComboBox() + self.environment_combobox.setObjectName("environments") + self.queue_combobox = QComboBox() + self.parameters_editor = AlgorithmParametersEditor(prefix_path) + + self.remap_button = QPushButton(QIcon(":/resources/remap.png"), "") + self.remap_button.setObjectName("remap") + self.remap_button.setToolTip(self.tr("Remap input and outputs")) + reset_button = QPushButton( + self.style().standardIcon(QStyle.SP_DialogResetButton), "" + ) + reset_button.setToolTip(self.tr("Reset values")) + self.parameters_button = QPushButton(self.tr("Parameters")) + self.parameters_button.setCheckable(True) + + button_layout = QHBoxLayout() + button_layout.addWidget(self.parameters_button) + button_layout.addStretch(1) + button_layout.addWidget(self.remap_button) + button_layout.addWidget(reset_button) + + groupbox = QGroupBox() + groupbox.setVisible(False) + self.parameters_layout = QFormLayout(groupbox) + self.parameters_layout.addRow(self.tr("Environment"), self.environment_combobox) + self.parameters_layout.addRow(self.tr("Queue"), self.queue_combobox) + self.parameters_layout.addRow(self.tr("Parameters"), self.parameters_editor) + + layout = QVBoxLayout(self) + layout.addWidget(self.algorithm_combobox) + layout.addLayout(button_layout) + layout.addWidget(groupbox) + + self.algorithm_combobox.currentIndexChanged.connect(self.dataChanged) + self.algorithm_combobox.currentTextChanged.connect(self.algorithmChanged) + self.algorithm_combobox.currentTextChanged.connect( + lambda algorithm_name: self.parameters_editor.setup(algorithm_name) + ) + self.environment_combobox.currentIndexChanged.connect(self.dataChanged) + self.environment_combobox.currentIndexChanged.connect( + lambda *args: setattr(self, "environment_changed", True) + ) + self.queue_combobox.currentIndexChanged.connect(self.dataChanged) + self.queue_combobox.currentIndexChanged.connect( + lambda *args: setattr(self, "queue_changed", True) + ) + self.parameters_editor.dataChanged.connect(self.dataChanged) + self.parameters_editor.parameterCountChanged.connect(self.__updateUi) + self.remap_button.clicked.connect(self.__remapIO) + reset_button.clicked.connect(lambda: self.load(self.json_object)) + self.parameters_button.toggled.connect(self.__updateUi) + self.parameters_button.toggled.connect(groupbox.setVisible) + + self.__updateUi() + + @pyqtSlot() + def __remapIO(self): + + status, mapping = IOMapperDialog.getIOMapping(self.prefix_path, self.dump()) + + if status and self.io_mapping != mapping: + self.io_mapping = mapping + self.dataChanged.emit() + + @pyqtSlot() + def __updateUi(self): + icon = ( + QStyle.SP_TitleBarShadeButton + if self.parameters_button.isChecked() + else QStyle.SP_TitleBarUnshadeButton + ) + self.parameters_button.setIcon(self.style().standardIcon(icon)) + + self.parameters_editor.setVisible(not self.parameters_editor.isEmpty()) + self.parameters_layout.labelForField(self.parameters_editor).setVisible( + not self.parameters_editor.isEmpty() + ) + + self.queue_combobox.setVisible(self._queue_enabled) + self.parameters_layout.labelForField(self.queue_combobox).setVisible( + self._queue_enabled + ) + + def algorithm(self): + return self.algorithm_combobox.currentText() + + algorithm = pyqtProperty(str, fget=algorithm, notify=algorithmChanged) + + def setAlgorithmModel(self, model): + self.algorithm_combobox.setModel(model) + + def setEnvironmentModel(self, model): + self.environment_combobox.setModel(model) + + def environmentModel(self): + return self.environment_combobox.model() + + def setQueueModel(self, model): + self.queue_combobox.setModel(model) + + def setQueueEnabled(self, enabled): + self._queue_enabled = enabled + self.__updateUi() + + def load(self, json_object): + self.json_object = copy.deepcopy(json_object) + self.io_mapping = {"inputs": json_object["inputs"]} + + outputs = json_object.get("outputs") + if outputs: + self.io_mapping["outputs"] = outputs + + algorithm_name = json_object["algorithm"] + environment = json_object.get("environment") + parameters = json_object.get("parameters") + + self.algorithm_combobox.setCurrentText(algorithm_name) + if self.algorithm_combobox.currentText() != algorithm_name: + raise RuntimeError( + "Algorithm {} not found in prefix".format(algorithm_name) + ) + self.parameters_editor.setup(algorithm_name) + + if environment: + env_text = "{} ({})".format(environment["name"], environment["version"]) + self.environment_combobox.setCurrentText(env_text) + else: + self.environment_combobox.setCurrentIndex(0) + + if parameters: + self.parameters_editor.load(parameters) + + self.environment_changed = False + self.queue_changed = False + self.parameters_changed = False + + def dump(self): + data = copy.deepcopy(self.json_object) + data["algorithm"] = self.algorithm_combobox.currentText() + + if self.environment_changed: + model = self.environment_combobox.model() + index = model.index(self.environment_combobox.currentIndex(), 0) + data["environment"] = model.environment(index) + + if self._queue_enabled and self.queue_changed: + data["queue"] = self.queue_combobox.currentText() + + parameters = self.parameters_editor.dump() + if parameters: + data["parameters"] = parameters + + data.update(self.io_mapping) + return data + + +class AbstractBlockEditor(AbstractBaseEditor): + """Common elements for editing a block""" + + algorithmChanged = pyqtSignal(str) + + def __init__(self, block_name, prefix_path, parent=None): + super().__init__(prefix_path, parent) + + self.block_name = block_name + + def setAlgorithmModel(self, model): + """Set algorithm model""" + + raise NotImplementedError + + def setEnvironmentModel(self, model): + """Set environment model""" + + raise NotImplementedError + + def setQueueModel(self, model): + """Setup queue model""" + + raise NotImplementedError + + +class BlockEditor(AbstractBlockEditor): + def __init__(self, block_name, prefix_path, parent=None): + super().__init__(block_name, prefix_path, parent) + + self.proxy_model = FieldPresenceFilterProxyModel("results", False) + self.properties_editor = ExecutionPropertiesEditor(prefix_path) + self.properties_editor.setAlgorithmModel(self.proxy_model) + + layout = QVBoxLayout(self) + layout.addWidget(QLabel(self.block_name)) + layout.addWidget(self.properties_editor) + + self.properties_editor.algorithmChanged.connect(self.algorithmChanged) + self.properties_editor.dataChanged.connect(self.dataChanged) + + self.prefixPathChanged.connect(self.proxy_model.invalidate) + + def setAlgorithmModel(self, model): + self.proxy_model.setSourceModel(model) + + def selectedAlgorithm(self): + return self.properties_editor.algorithm + + def setEnvironmentModel(self, model): + self.properties_editor.setEnvironmentModel(model) + + def environmentModel(self): + return self.properties_editor.environmentModel() + + def setQueueModel(self, model): + self.properties_editor.setQueueModel(model) + + def load(self, json_object): + self.properties_editor.load(json_object) + + def dump(self): + return self.properties_editor.dump() + + +class AnalyzerBlockEditor(BlockEditor): + def __init__(self, block_name, prefix_path, parent=None): + super().__init__(block_name, prefix_path, parent) + + self.proxy_model.setFieldFilter("results", True) + + +class LoopBlockEditor(AbstractBlockEditor): + def __init__(self, block_name, prefix_path, parent=None): + super().__init__(block_name, prefix_path, parent) + + self.processor_model = FieldPresenceFilterProxyModel( + "type", True, ".*loop_processor" + ) + self.evaluator_model = FieldPresenceFilterProxyModel( + "type", True, ".*loop_evaluator" + ) + + self.processor_properties_editor = ExecutionPropertiesEditor(prefix_path) + self.processor_properties_editor.setAlgorithmModel(self.processor_model) + self.evaluator_properties_editor = ExecutionPropertiesEditor(prefix_path) + self.evaluator_properties_editor.setAlgorithmModel(self.evaluator_model) + self.evaluator_properties_editor.setQueueEnabled(False) + + processor_groupbox = QGroupBox(self.tr("Processor")) + evaluator_groupbox = QGroupBox(self.tr("Evaluator")) + + processor_layout = QVBoxLayout(processor_groupbox) + processor_layout.addWidget(self.processor_properties_editor) + + evaluator_layout = QVBoxLayout(evaluator_groupbox) + evaluator_layout.addWidget(self.evaluator_properties_editor) + + layout = QVBoxLayout(self) + layout.addWidget(QLabel(self.block_name)) + layout.addWidget(processor_groupbox) + layout.addWidget(evaluator_groupbox) + + self.processor_properties_editor.algorithmChanged.connect(self.algorithmChanged) + self.processor_properties_editor.dataChanged.connect(self.dataChanged) + self.evaluator_properties_editor.algorithmChanged.connect(self.algorithmChanged) + self.evaluator_properties_editor.dataChanged.connect(self.dataChanged) + + self.prefixPathChanged.connect(self.processor_model.invalidate) + self.prefixPathChanged.connect(self.evaluator_model.invalidate) + + def setAlgorithmModel(self, model): + self.processor_model.setSourceModel(model) + self.evaluator_model.setSourceModel(model) + + def selectedAlgorithms(self): + return { + self.processor_properties_editor.algorithm, + self.evaluator_properties_editor.algorithm, + } + + def setEnvironmentModel(self, model): + for editor in [ + self.processor_properties_editor, + self.evaluator_properties_editor, + ]: + editor.setEnvironmentModel(model) + + def environmentModel(self): + return self.processor_properties_editor.environmentModel() + + def setQueueModel(self, model): + self.processor_properties_editor.setQueueModel(model) + + def load(self, json_object): + processor_data = { + key[len(PROCESSOR_PREFIX) :]: value + for key, value in json_object.items() + if key.startswith(PROCESSOR_PREFIX) + } + evaluator_data = { + key[len(EVALUATOR_PREFIX) :]: value + for key, value in json_object.items() + if key.startswith(EVALUATOR_PREFIX) + } + + self.processor_properties_editor.load(processor_data) + self.evaluator_properties_editor.load(evaluator_data) + + def dump(self): + data = {} + processor_data = self.processor_properties_editor.dump() + data.update( + {f"{PROCESSOR_PREFIX}{key}": value for key, value in processor_data.items()} + ) + evaluator_data = self.evaluator_properties_editor.dump() + data.update( + {f"{EVALUATOR_PREFIX}{key}": value for key, value in evaluator_data.items()} + ) + return data + + +class EditorGroupBox(QGroupBox): + """Container widget for the GlobalParametersEditor""" + + def __init__(self, name, editor, parent=None): + super().__init__(parent) + + self.editor = editor + + name_label = QLabel(name) + reset_button = QPushButton( + self.style().standardIcon(QStyle.SP_DialogResetButton), "" + ) + reset_button.setToolTip(self.tr("Reset values")) + + title_layout = QHBoxLayout() + title_layout.addWidget(name_label) + title_layout.addWidget(reset_button) + title_layout.addStretch(1) + + layout = QVBoxLayout(self) + layout.addLayout(title_layout) + layout.addWidget(editor) + + reset_button.clicked.connect(editor.reset) + + +class GlobalParametersEditor(AbstractBaseEditor): + """Widget showing all the parameters that can be set""" + + dataChanged = pyqtSignal() + + def __init__(self, prefix_path, parent=None): + super().__init__(prefix_path, parent) + + self.json_object = {} + self.environment_changed = False + self.queue_changed = False + + self.environment_combobox = QComboBox() + self.queue_combobox = QComboBox() + self.parameters_editor_listwidget = EditorListWidget() + + form_layout = QFormLayout() + form_layout.addRow(self.tr("Environment"), self.environment_combobox) + form_layout.addRow(self.tr("Queue"), self.queue_combobox) + + layout = QVBoxLayout(self) + layout.addLayout(form_layout) + layout.addWidget(self.parameters_editor_listwidget) + layout.addStretch(1) + + self.environment_combobox.currentIndexChanged.connect(self.dataChanged) + self.environment_combobox.currentIndexChanged.connect( + lambda *args: setattr(self, "environment_changed", True) + ) + self.queue_combobox.currentIndexChanged.connect(self.dataChanged) + self.queue_combobox.currentIndexChanged.connect( + lambda *args: setattr(self, "queue_changed", True) + ) + self.parameters_editor_listwidget.dataChanged.connect(self.dataChanged) + + def setEnvironmentModel(self, model): + self.environment_combobox.setModel(model) + + def setQueueModel(self, model): + self.queue_combobox.setModel(model) + + def clear(self): + self.environment_combobox.setCurrentIndex(0) + self.parameters_editor_listwidget.clear() + + def setup(self, algorithms): + current_algorithm_widgets = { + widget.editor.algorithm_name: widget + for widget in self.parameters_editor_listwidget.widget_list + } + current_algorithms = set(current_algorithm_widgets.keys()) + + to_add = algorithms.difference(current_algorithms) + to_remove = current_algorithms.difference(algorithms) + + for algorithm_name in to_remove: + self.parameters_editor_listwidget.removeWidget( + current_algorithm_widgets[algorithm_name] + ) + self.json_object.pop(algorithm_name, None) + + for algorithm_name in to_add: + algorithm = AssetType.ALGORITHM.klass(self.prefix_path, algorithm_name) + if algorithm.valid and algorithm.parameters: + editor = AlgorithmParametersEditor(self.prefix_path) + editor.setDumpEditOnlyEnabled(False) + editor.setup(algorithm_name) + editor.dataChanged.connect(self.dataChanged) + + global_parameter_editor = EditorGroupBox(algorithm_name, editor) + + self.parameters_editor_listwidget.addWidget(global_parameter_editor) + + def load(self, json_object): + self.json_object = copy.deepcopy(json_object) + + environment = self.json_object["environment"] + env_text = "{} ({})".format(environment["name"], environment["version"]) + self.environment_combobox.setCurrentText(env_text) + + parameters = [ + item + for item in self.json_object.items() + if item[0] not in ["queue", "environment"] + ] + + for data in parameters: + name, data = data + editor = None + for widget in self.parameters_editor_listwidget.widget_list: + if widget.editor.algorithm_name == name: + editor = widget.editor + break + if editor is None: + raise RuntimeError("Mismatch between globals data and block data") + editor.load(data) + + self.environment_changed = False + self.queue_changed = False + self.parameters_changed = False + + def dump(self): + data = copy.deepcopy(self.json_object) + + if self.environment_changed: + model = self.environment_combobox.model() + index = model.index(self.environment_combobox.currentIndex(), 0) + data["environment"] = model.environment(index) + if self.queue_changed: + data["queue"] = self.queue_combobox.currentText() + + for widget in self.parameters_editor_listwidget.widget_list: + editor = widget.editor + data[editor.algorithm_name] = editor.dump() + + return data + + @frozen class ExperimentEditor(AbstractAssetEditor): def __init__(self, parent=None): @@ -36,10 +1038,187 @@ class ExperimentEditor(AbstractAssetEditor): self.setObjectName(self.__class__.__name__) self.set_title(self.tr("Experiment")) + self.processing_env_model = EnvironmentModel() + self.queue_model = QStringListModel() + self.queue_model.setStringList(["Local"]) + + self.dataset_model = DatasetModel() + + self.algorithm_model = AssetModel() + self.algorithm_model.setLatestOnlyEnabled(False) + self.algorithm_model.asset_type = AssetType.ALGORITHM + + self.datasets_widget = ContainerWidget() + self.blocks_widget = ContainerWidget() + self.loops_widget = ContainerWidget() + self.analyzers_widget = ContainerWidget() + self.globalparameters_widget = GlobalParametersEditor(self.prefixPath()) + self.globalparameters_widget.setEnvironmentModel(self.processing_env_model) + self.globalparameters_widget.setQueueModel(self.queue_model) + + self.tabwidget = QTabWidget() + self.tabwidget.addTab(self.datasets_widget, self.tr("Datasets")) + self.tabwidget.addTab(self.blocks_widget, self.tr("Blocks")) + self.loops_widget_index = self.tabwidget.addTab( + self.loops_widget, self.tr("Loops") + ) + self.tabwidget.addTab(self.analyzers_widget, self.tr("Analyzers")) + self.tabwidget.addTab( + self.globalparameters_widget, self.tr("Global parameters") + ) + + self.layout().addWidget(self.tabwidget) + + self.contextChanged.connect(self.__update) + + for widget in [ + self.datasets_widget, + self.blocks_widget, + self.loops_widget, + self.analyzers_widget, + self.globalparameters_widget, + ]: + widget.dataChanged.connect(self.dataChanged) + + @pyqtSlot() + def __update(self): + for object_ in [ + self.algorithm_model, + self.dataset_model, + self.datasets_widget, + self.blocks_widget, + self.loops_widget, + self.analyzers_widget, + self.globalparameters_widget, + ]: + object_.setPrefixPath(self.prefix_path) + + self.processing_env_model.setContext(self.context) + + @pyqtSlot() + def __onAlgorithmChanged(self): + algorithms = { + editor.properties_editor.algorithm + for editor in self.blocks_widget.widget_list + + self.analyzers_widget.widget_list + } + + for editor in self.loops_widget.widget_list: + algorithms.add(editor.processor_properties_editor.algorithm) + algorithms.add(editor.evaluator_properties_editor.algorithm) + self.globalparameters_widget.setup(algorithms) + def _load_json(self, json_object): """Load the json object passed as parameter""" - pass + + for widget in [ + self.datasets_widget, + self.blocks_widget, + self.loops_widget, + self.analyzers_widget, + self.globalparameters_widget, + ]: + widget.clear() + + datasets = json_object.get("datasets") + if datasets: + for name, dataset in datasets.items(): + editor = DatasetEditor(name, self.prefix_path) + editor.setDatasetModel(self.dataset_model) + editor.load(dataset) + self.datasets_widget.addWidget(editor) + + used_algorithms = set() + + for type_, container, editor_klass in [ + ("blocks", self.blocks_widget, BlockEditor), + ("loops", self.loops_widget, LoopBlockEditor), + ("analyzers", self.analyzers_widget, AnalyzerBlockEditor), + ]: + items = json_object.get(type_) + if items: + for name, data in items.items(): + editor = editor_klass(name, self.prefix_path) + editor.setAlgorithmModel(self.algorithm_model) + editor.setEnvironmentModel(self.processing_env_model) + editor.setQueueModel(self.queue_model) + editor.load(data) + editor.algorithmChanged.connect(self.__onAlgorithmChanged) + container.addWidget(editor) + + if type_ == "loops": + used_algorithms |= editor.selectedAlgorithms() + else: + used_algorithms.add(editor.selectedAlgorithm()) + + self.globalparameters_widget.setup(used_algorithms) + self.tabwidget.tabBar().setTabEnabled( + self.loops_widget_index, "loops" in json_object + ) + globals_ = json_object.get("globals") + if globals_: + self.globalparameters_widget.load(globals_) def _dump_json(self): """Returns the json representation of the asset""" - return {} + + def __filter_parameters(blocks, globals_): + """If algorithm parameters matches globals keep only the globals version""" + + algorithms = [ + key for key in globals_.keys() if key not in ["environment", "queue"] + ] + for block_name, block_data in blocks.items(): + for prefix in ["", PROCESSOR_PREFIX, EVALUATOR_PREFIX]: + algorithm_field = f"{prefix}algorithm" + parameter_field = f"{prefix}parameters" + if algorithm_field in block_data: + algorithm_name = block_data[algorithm_field] + + if algorithm_name in algorithms: + block_parameters = block_data.get(parameter_field) + if block_parameters: + global_parameters = globals_[algorithm_name] + for ( + parameter_name, + parameter_value, + ) in global_parameters.items(): + if ( + block_parameters[parameter_name] + == parameter_value + ): + block_parameters.pop(parameter_name) + if not block_parameters: + block_data.pop(parameter_field) + + def __filter_environment(blocks, globals_): + globals_environment = globals_["environment"] + + for block_name, block_data in blocks.items(): + for prefix in ["", PROCESSOR_PREFIX, EVALUATOR_PREFIX]: + environment_key = f"{prefix}environment" + + block_environment = block_data.get(environment_key) + if block_environment and block_environment == globals_environment: + block_data.pop(environment_key) + + analyzers = self.analyzers_widget.dump() + blocks = self.blocks_widget.dump() + loops = self.loops_widget.dump() + globals_ = self.globalparameters_widget.dump() + + for item in [analyzers, blocks, loops]: + __filter_parameters(item, globals_) + __filter_environment(item, globals_) + + data = { + "analyzers": analyzers, + "blocks": blocks, + "datasets": self.datasets_widget.dump(), + "globals": globals_, + } + + if loops: + data["loops"] = loops + + return data -- GitLab