def test_read_only(self): model = DomainModel() domain = Domain([ContinuousVariable(x) for x in "abc"]) model.set_domain(domain) index = model.index(0, 0) self.assertRaises(TypeError, model.append, 42) self.assertRaises(TypeError, model.extend, [42]) self.assertRaises(TypeError, model.insert, 0, 42) self.assertRaises(TypeError, model.remove, 0) self.assertRaises(TypeError, model.pop) self.assertRaises(TypeError, model.clear) self.assertRaises(TypeError, model.reverse) self.assertRaises(TypeError, model.sort) with self.assertRaises(TypeError): model[0] = 1 with self.assertRaises(TypeError): del model[0] self.assertFalse(model.setData(index, domain[0], Qt.EditRole)) self.assertTrue(model.setData(index, "foo", Qt.ToolTipRole)) self.assertFalse( model.setItemData(index, { Qt.EditRole: domain[0], Qt.ToolTipRole: "foo" })) self.assertTrue(model.setItemData(index, {Qt.ToolTipRole: "foo"})) self.assertFalse(model.insertRows(0, 1)) self.assertSequenceEqual(model, domain) self.assertFalse(model.removeRows(0, 1)) self.assertSequenceEqual(model, domain)
def test_read_only(self): model = DomainModel() domain = Domain([ContinuousVariable(x) for x in "abc"]) model.set_domain(domain) index = model.index(0, 0) self.assertRaises(TypeError, model.append, 42) self.assertRaises(TypeError, model.extend, [42]) self.assertRaises(TypeError, model.insert, 0, 42) self.assertRaises(TypeError, model.remove, 0) self.assertRaises(TypeError, model.pop) self.assertRaises(TypeError, model.clear) self.assertRaises(TypeError, model.reverse) self.assertRaises(TypeError, model.sort) with self.assertRaises(TypeError): model[0] = 1 with self.assertRaises(TypeError): del model[0] self.assertRaises(TypeError, model.setData, index, domain[0]) self.assertTrue(model.setData(index, "foo", Qt.ToolTipRole)) self.assertRaises(TypeError, model.setItemData, index, {Qt.EditRole: domain[0], Qt.ToolTipRole: "foo"}) self.assertTrue(model.setItemData(index, {Qt.ToolTipRole: "foo"})) self.assertRaises(TypeError, model.insertRows, 0, 0) self.assertRaises(TypeError, model.removeRows, 0, 0)
class OWGroupBy(OWWidget, ConcurrentWidgetMixin): name = "Group by" description = "" category = "Transform" icon = "icons/GroupBy.svg" keywords = ["aggregate", "group by"] class Inputs: data = Input("Data", Table, doc="Input data table") class Outputs: data = Output("Data", Table, doc="Aggregated data") class Error(OWWidget.Error): unexpected_error = Msg("{}") settingsHandler = DomainContextHandler() gb_attrs: List[Variable] = ContextSetting([]) aggregations: Dict[Variable, Set[str]] = ContextSetting({}) auto_commit: bool = Setting(True) def __init__(self): super().__init__() ConcurrentWidgetMixin.__init__(self) self.data = None self.result = None self.gb_attrs_model = DomainModel(separators=False, ) self.agg_table_model = VarTableModel(self) self.agg_checkboxes = {} self.__init_control_area() self.__init_main_area() def __init_control_area(self) -> None: """Init all controls in the control area""" box = gui.vBox(self.controlArea, "Group by") self.gb_attrs_view = AggregateListViewSearch( selectionMode=QListView.ExtendedSelection) self.gb_attrs_view.setModel(self.gb_attrs_model) self.gb_attrs_view.selectionModel().selectionChanged.connect( self.__gb_changed) box.layout().addWidget(self.gb_attrs_view) gui.auto_send(self.buttonsArea, self, "auto_commit") def __init_main_area(self) -> None: """Init all controls in the main area""" # aggregation table self.agg_table_view = tableview = QTableView() tableview.setModel(self.agg_table_model) tableview.setSelectionBehavior(QAbstractItemView.SelectRows) tableview.selectionModel().selectionChanged.connect( self.__rows_selected) tableview.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) vbox = gui.vBox(self.mainArea, " ") vbox.layout().addWidget(tableview) # aggregations checkboxes grid_layout = QGridLayout() gui.widgetBox(self.mainArea, orientation=grid_layout, box="Aggregations") col = 0 row = 0 break_rows = (5, 5, 99) for agg in AGGREGATIONS: self.agg_checkboxes[agg] = cb = CheckBox(agg, self) cb.setDisabled(True) cb.stateChanged.connect(partial(self.__aggregation_changed, agg)) grid_layout.addWidget(cb, row, col) row += 1 if row == break_rows[col]: row = 0 col += 1 ############ # Callbacks def __rows_selected(self) -> None: """Callback for table selection change; update checkboxes""" selected_attrs = self.get_selected_attributes() types = {type(attr) for attr in selected_attrs} active_aggregations = [ self.aggregations[attr] for attr in selected_attrs ] for agg, cb in self.agg_checkboxes.items(): cb.setDisabled(not types & AGGREGATIONS[agg].types) activated = {agg in a for a in active_aggregations} with block_signals(cb): # check if aggregation active for all selected attributes, # partially check if active for some else uncheck cb.setCheckState(Qt.Checked if activated == {True} else ( Qt.Unchecked if activated == {False} else Qt.PartiallyChecked)) def __gb_changed(self) -> None: """ Callback for Group-by attributes selection change; update attribute and call commit """ rows = self.gb_attrs_view.selectionModel().selectedRows() values = self.gb_attrs_view.model()[:] self.gb_attrs = [values[row.row()] for row in sorted(rows)] # everything cached in result should be recomputed on gb change self.result = Result() self.commit.deferred() def __aggregation_changed(self, agg: str) -> None: """ Callback for aggregation change; update aggregations dictionary and call commit """ selected_attrs = self.get_selected_attributes() for attr in selected_attrs: if self.agg_checkboxes[agg].isChecked( ) and self.__aggregation_compatible(agg, attr): self.aggregations[attr].add(agg) else: self.aggregations[attr].discard(agg) self.agg_table_model.update_aggregation(attr) self.commit.deferred() @Inputs.data def set_data(self, data: Table) -> None: self.closeContext() self.data = data # reset states self.cancel() self.result = Result() self.Outputs.data.send(None) self.gb_attrs_model.set_domain(data.domain if data else None) self.gb_attrs = data.domain[:1] if data else [] self.aggregations = ({ attr: DEFAULT_AGGREGATIONS[type(attr)].copy() for attr in data.domain.variables + data.domain.metas } if data else {}) default_aggregations = self.aggregations.copy() self.openContext(self.data) # restore aggregations self.aggregations.update({ k: v for k, v in default_aggregations.items() if k not in self.aggregations }) # update selections in widgets and re-plot self.agg_table_model.set_domain(data.domain if data else None) self._set_gb_selection() self.commit.now() ######################### # Task connected methods @gui.deferred def commit(self) -> None: self.Error.clear() self.Warning.clear() if self.data: self.start(_run, self.data, self.gb_attrs, self.aggregations, self.result) def on_done(self, result: Result) -> None: self.result = result self.Outputs.data.send(result.result_table) def on_partial_result(self, result: Result) -> None: # store result in case the task is canceled and on_done is not called self.result = result def on_exception(self, ex: Exception): self.Error.unexpected_error(str(ex)) ################### # Helper methods def get_selected_attributes(self): """Get select attributes in the table""" selection_model = self.agg_table_view.selectionModel() sel_rows = selection_model.selectedRows() vars_ = self.data.domain.variables + self.data.domain.metas return [vars_[index.row()] for index in sel_rows] def _set_gb_selection(self) -> None: """Set selection in groupby list according to self.gb_attrs""" sm = self.gb_attrs_view.selectionModel() values = self.gb_attrs_model[:] with disconnected(sm.selectionChanged, self.__gb_changed): for val in self.gb_attrs: index = values.index(val) model_index = self.gb_attrs_model.index(index, 0) sm.select(model_index, QItemSelectionModel.Select) @staticmethod def __aggregation_compatible(agg, attr): """Check a compatibility of aggregation with the variable""" return type(attr) in AGGREGATIONS[agg].types