def showPopup(self): # type: () -> None """ Reimplemented from QComboBox.showPopup Popup up a customized view and filter edit line. Note ---- The .popup(), .lineEdit(), .completer() of the base class are not used. """ if self.__popup is not None: # We have user entered state that cannot be disturbed # (entered filter text, scroll offset, ...) return # pragma: no cover if self.count() == 0: return opt = QStyleOptionComboBox() self.initStyleOption(opt) popup = QListView( uniformItemSizes=True, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAsNeeded, iconSize=self.iconSize(), ) popup.setFocusProxy(self.__searchline) popup.setParent(self, Qt.Popup | Qt.FramelessWindowHint) popup.setItemDelegate(_ComboBoxListDelegate(popup)) proxy = QSortFilterProxyModel( popup, filterCaseSensitivity=Qt.CaseInsensitive ) proxy.setFilterKeyColumn(self.modelColumn()) proxy.setSourceModel(self.model()) popup.setModel(proxy) root = proxy.mapFromSource(self.rootModelIndex()) popup.setRootIndex(root) self.__popup = popup self.__proxy = proxy self.__searchline.setText("") self.__searchline.setPlaceholderText("Filter...") self.__searchline.setVisible(True) self.__searchline.textEdited.connect(proxy.setFilterFixedString) style = self.style() # type: QStyle popuprect_origin = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxListBoxPopup, self ) # type: QRect popuprect_origin = QRect( self.mapToGlobal(popuprect_origin.topLeft()), popuprect_origin.size() ) editrect = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self ) # type: QRect self.__searchline.setGeometry(editrect) desktop = QApplication.desktop() screenrect = desktop.availableGeometry(self) # type: QRect # get the height for the view listrect = QRect() for i in range(min(proxy.rowCount(root), self.maxVisibleItems())): index = proxy.index(i, self.modelColumn(), root) if index.isValid(): listrect = listrect.united(popup.visualRect(index)) if listrect.height() >= screenrect.height(): break window = popup.window() # type: QWidget window.ensurePolished() if window.layout() is not None: window.layout().activate() else: QApplication.sendEvent(window, QEvent(QEvent.LayoutRequest)) margins = qwidget_margin_within(popup.viewport(), window) height = (listrect.height() + 2 * popup.spacing() + margins.top() + margins.bottom()) popup_size = (QSize(popuprect_origin.width(), height) .expandedTo(window.minimumSize()) .boundedTo(window.maximumSize()) .boundedTo(screenrect.size())) popuprect = QRect(popuprect_origin.bottomLeft(), popup_size) popuprect = dropdown_popup_geometry( popuprect, popuprect_origin, screenrect) popup.setGeometry(popuprect) current = proxy.mapFromSource( self.model().index(self.currentIndex(), self.modelColumn(), self.rootModelIndex())) popup.setCurrentIndex(current) popup.scrollTo(current, QAbstractItemView.EnsureVisible) popup.show() popup.setFocus(Qt.PopupFocusReason) popup.installEventFilter(self) popup.viewport().installEventFilter(self) popup.viewport().setMouseTracking(True) self.update() self.__popupTimer.restart()
class SelectionSetsWidget(QFrame): """ Widget for managing multiple stored item selections """ selectionModified = Signal(bool) def __init__(self, parent): QFrame.__init__(self, parent) self.setContentsMargins(0, 0, 0, 0) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) self._setNameLineEdit = QLineEdit(self) layout.addWidget(self._setNameLineEdit) self._setListView = QListView(self) self._listModel = QStandardItemModel(self) self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSourceModel(self._listModel) self._setListView.setModel(self._proxyModel) self._setListView.setItemDelegate(ListItemDelegate(self)) self._setNameLineEdit.textChanged.connect( self._proxyModel.setFilterFixedString) self._completer = QCompleter(self._listModel, self) self._setNameLineEdit.setCompleter(self._completer) self._listModel.itemChanged.connect(self._onSetNameChange) layout.addWidget(self._setListView) buttonLayout = QHBoxLayout() self._addAction = QAction("+", self, toolTip="Add a new sort key") self._updateAction = QAction("Update", self, toolTip="Update/save current selection") self._removeAction = QAction("\u2212", self, toolTip="Remove selected sort key.") self._addToolButton = QToolButton(self) self._updateToolButton = QToolButton(self) self._removeToolButton = QToolButton(self) self._updateToolButton.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self._addToolButton.setDefaultAction(self._addAction) self._updateToolButton.setDefaultAction(self._updateAction) self._removeToolButton.setDefaultAction(self._removeAction) buttonLayout.addWidget(self._addToolButton) buttonLayout.addWidget(self._updateToolButton) buttonLayout.addWidget(self._removeToolButton) layout.addLayout(buttonLayout) self.setLayout(layout) self._addAction.triggered.connect(self.addCurrentSelection) self._updateAction.triggered.connect(self.updateSelectedSelection) self._removeAction.triggered.connect(self.removeSelectedSelection) self._setListView.selectionModel().selectionChanged.connect( self._onListViewSelectionChanged) self.selectionModel = None self._selections = [] def sizeHint(self): size = QFrame.sizeHint(self) return QSize(size.width(), 150) def _onSelectionChanged(self, selected, deselected): self.setSelectionModified(True) def _onListViewSelectionChanged(self, selected, deselected): try: index = self._setListView.selectedIndexes()[0] except IndexError: return self.commitSelection(self._proxyModel.mapToSource(index).row()) def _onSetNameChange(self, item): self.selections[item.row()].name = str(item.text()) def _setButtonStates(self, val): self._updateToolButton.setEnabled(val) def setSelectionModel(self, selectionModel): if self.selectionModel: self.selectionModel.selectionChanged.disconnect( self._onSelectionChanged) self.selectionModel = selectionModel self.selectionModel.selectionChanged.connect(self._onSelectionChanged) def addCurrentSelection(self): item = self.addSelection( SelectionByKey(self.selectionModel.selection(), name="New selection", key=(1, 2, 3, 10))) index = self._proxyModel.mapFromSource(item.index()) self._setListView.setCurrentIndex(index) self._setListView.edit(index) self.setSelectionModified(False) def removeSelectedSelection(self): i = self._proxyModel.mapToSource( self._setListView.currentIndex()).row() self._listModel.takeRow(i) del self.selections[i] def updateCurentSelection(self): i = self._proxyModel.mapToSource( self._setListView.selectedIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def addSelection(self, selection, name=""): self._selections.append(selection) item = QStandardItem(selection.name) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listModel.appendRow(item) self.setSelectionModified(False) return item def updateSelectedSelection(self): i = self._proxyModel.mapToSource( self._setListView.currentIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def setSelectionModified(self, val): self._selectionModified = val self._setButtonStates(val) self.selectionModified.emit(bool(val)) def commitSelection(self, index): selection = self.selections[index] selection.select(self.selectionModel) def setSelections(self, selections): self._listModel.clear() for selection in selections: self.addSelection(selection) def selections(self): return self._selections selections = property(selections, setSelections)
class OWPythonScript(widget.OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." icon = "icons/PythonScript.svg" priority = 3150 inputs = [ ("in_data", Orange.data.Table, "setExampleTable", widget.Default), # ("in_distance", Orange.misc.SymMatrix, "setDistanceMatrix", # widget.Default), ("in_learner", Learner, "setLearner", widget.Default), ("in_classifier", Model, "setClassifier", widget.Default), ("in_object", object, "setObject") ] outputs = [ ( "out_data", Orange.data.Table, ), # ("out_distance", Orange.misc.SymMatrix, ), ( "out_learner", Learner, ), ("out_classifier", Model, widget.Dynamic), ("out_object", object, widget.Dynamic) ] libraryListSource = \ Setting([Script("Hello world", "print('Hello world')\n")]) currentScriptIndex = Setting(0) splitterState = Setting(None) auto_execute = Setting(False) def __init__(self): super().__init__() self.in_data = None self.in_distance = None self.in_learner = None self.in_classifier = None self.in_object = None self.auto_execute = False for s in self.libraryListSource: s.flags = 0 self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + \ "<li>".join(t.name for t in self.inputs) + \ "</ul></p><p>Output variables:<ul><li>" + \ "<li>".join(t.name for t in self.outputs) + \ "</ul></p>" ) self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.auto_commit(self.controlArea, self, "auto_execute", "Execute", auto_label="Auto Execute") self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.vBox(self, 'Python Script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) select_row(self.libraryView, self.currentScriptIndex) self.splitCanvas.setSizes([2, 1]) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) def setExampleTable(self, et): self.in_data = et def setDistanceMatrix(self, dm): self.in_distance = dm def setLearner(self, learner): self.in_learner = learner def setClassifier(self, classifier): self.in_classifier = classifier def setObject(self, obj): self.in_object = obj def handleNewSignals(self): self.unconditional_commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *args): self.libraryList.append(Script("New script", "", 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *args): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)') if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *args): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *args): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if type(script) != Script: script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *args): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def onSpliterMoved(self, pos, ind): self.splitterState = bytes(self.splitCanvas.saveState()) def updateSelecetdScriptState(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] self.libraryList[index] = Script(script.name, self.text.toPlainText(), 0) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)') if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = dict([(i.name, getattr(self, i.name, None)) for i in self.inputs]) d.update(dict([(o.name, None) for o in self.outputs])) return d def commit(self): self._script = str(self.text.toPlainText()) lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for out in self.outputs: signal = out.name self.send(signal, self.console.locals.get(signal, None))
class OWFeatureConstructor(OWWidget): name = "Feature Constructor" description = "Construct new features (data columns) from a set of " \ "existing features in the input data set." icon = "icons/FeatureConstructor.svg" class Inputs: data = Input("Data", Orange.data.Table) class Outputs: data = Output("Data", Orange.data.Table) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) EDITORS = [ (ContinuousDescriptor, ContinuousFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor) ] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "Variable Definitions") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget( sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) ) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton( "New", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New ) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def reserved_names(): varnames = [] if self.data is not None: varnames = [var.name for var in self.data.domain.variables + self.data.domain.metas] varnames += [desc.name for desc in self.featuremodel] return set(varnames) def generate_newname(fmt): return unique_name(fmt, reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("Numeric") cont.triggered.connect( lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3)) ) disc = menu.addAction("Categorical") disc.triggered.connect( lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", ("A", "B"), -1, False)) ) string = menu.addAction("Text") string.triggered.connect( lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), "")) ) menu.addSeparator() self.duplicateaction = menu.addAction("Duplicate Selected Variable") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton( "Remove", toolTip="Remove selected variable", minimumWidth=120, shortcut=QKeySequence.Delete ) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView( minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) ) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged ) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) box = gui.hBox(self.controlArea) gui.rubber(box) commit = gui.button(box, self, "Send", callback=self.apply, default=True) commit.setMinimumWidth(180) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: editor = self.editorstack.currentWidget() self.featuremodel[self.currentIndex] = editor.editorData() self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data if self.data is not None: descriptors = list(self.descriptors) currindex = self.currentIndex self.descriptors = [] self.currentIndex = -1 self.openContext(data) if descriptors != self.descriptors or \ self.currentIndex != currindex: # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect( self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect( self._on_selectedVariableChanged) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.Outputs.data.send(None) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and len(self.featuremodel) > 0: # Deleting the last item clears selection self.setCurrentIndex(len(self.featuremodel) - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) def check_attrs_values(self, attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) source_vars = self.data.domain.variables + self.data.domain.metas new_variables = construct_variables(desc, source_vars) attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas) ) try: data = self.data.transform(new_domain) except Exception as err: log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) return disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): items[feature.name] = "{} (categorical with values {}{})".format( feature.expression, feature.values, "; ordered" * feature.ordered) elif isinstance(feature, ContinuousDescriptor): items[feature.name] = "{} (numeric)".format(feature.expression) else: items[feature.name] = "{} (text)".format(feature.expression) self.report_items( report.plural("Constructed feature{s}", len(items)), items)
class OWDiscretize(widget.OWWidget): name = "Discretize" description = "Discretize the numeric data features." icon = "icons/Discretize.svg" inputs = [InputSignal("Data", Orange.data.Table, "set_data", doc="Input data table")] outputs = [OutputSignal("Data", Orange.data.Table, doc="Table with discretized features")] settingsHandler = settings.DomainContextHandler() saved_var_states = settings.ContextSetting({}) default_method = settings.Setting(2) default_k = settings.Setting(3) autosend = settings.Setting(True) #: Discretization methods Default, Leave, MDL, EqualFreq, EqualWidth, Remove, Custom = range(7) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() #: input data self.data = None #: Current variable discretization state self.var_state = {} #: Saved variable discretization settings (context setting) self.saved_var_states = {} self.method = 0 self.k = 5 box = gui.vBox(self.controlArea, self.tr("Default Discretization")) self.default_bbox = rbox = gui.radioButtons( box, self, "default_method", callback=self._default_disc_changed) rb = gui.hBox(rbox) self.left = gui.vBox(rb) right = gui.vBox(rb) rb.layout().setStretch(0, 1) rb.layout().setStretch(1, 1) options = self.options = [ self.tr("Default"), self.tr("Leave numeric"), self.tr("Entropy-MDL discretization"), self.tr("Equal-frequency discretization"), self.tr("Equal-width discretization"), self.tr("Remove numeric variables") ] for opt in options[1:]: t = gui.appendRadioButton(rbox, opt) # This condition is ugly, but it keeps the same order of # options for backward compatibility of saved schemata [right, self.left][opt.startswith("Equal")].layout().addWidget(t) gui.separator(right, 18, 18) def _intbox(widget, attr, callback): box = gui.indentedBox(widget) s = gui.spin( box, self, attr, minv=2, maxv=10, label="Num. of intervals:", callback=callback) s.setMaximumWidth(60) s.setAlignment(Qt.AlignRight) gui.rubber(s.box) return box.box self.k_general = _intbox(self.left, "default_k", self._default_disc_changed) self.k_general.layout().setContentsMargins(0, 0, 0, 0) vlayout = QHBoxLayout() box = gui.widgetBox( self.controlArea, "Individual Attribute Settings", orientation=vlayout, spacing=8 ) # List view with all attributes self.varview = QListView(selectionMode=QListView.ExtendedSelection) self.varview.setItemDelegate(DiscDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._var_selection_changed ) vlayout.addWidget(self.varview) # Controls for individual attr settings self.bbox = controlbox = gui.radioButtons( box, self, "method", callback=self._disc_method_changed ) vlayout.addWidget(controlbox) for opt in options[:5]: gui.appendRadioButton(controlbox, opt) self.k_specific = _intbox(controlbox, "k", self._disc_method_changed) gui.appendRadioButton(controlbox, "Remove attribute") gui.rubber(controlbox) controlbox.setEnabled(False) self.controlbox = controlbox box = gui.auto_commit( self.controlArea, self, "autosend", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 20) box.layout().insertWidget(0, self.report_button) self._update_spin_positions() def set_data(self, data): self.closeContext() self.data = data if self.data is not None: self._initialize(data) self.openContext(data) # Restore the per variable discretization settings self._restore(self.saved_var_states) # Complete the induction of cut points self._update_points() else: self._clear() self.unconditional_commit() def _initialize(self, data): # Initialize the default variable states for new data. self.class_var = data.domain.class_var cvars = [var for var in data.domain if var.is_continuous] self.varmodel[:] = cvars class_var = data.domain.class_var has_disc_class = data.domain.has_discrete_class self.default_bbox.buttons[self.MDL - 1].setEnabled(has_disc_class) self.bbox.buttons[self.MDL].setEnabled(has_disc_class) # If the newly disabled MDL button is checked then change it if not has_disc_class and self.default_method == self.MDL - 1: self.default_method = 0 if not has_disc_class and self.method == self.MDL: self.method = 0 # Reset (initialize) the variable discretization states. self._reset() def _restore(self, saved_state): # Restore variable states from a saved_state dictionary. def_method = self._current_default_method() for i, var in enumerate(self.varmodel): key = variable_key(var) if key in saved_state: state = saved_state[key] if isinstance(state.method, Default): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _reset(self): # restore the individual variable settings back to defaults. def_method = self._current_default_method() self.var_state = {} for i in range(len(self.varmodel)): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _set_var_state(self, index, state): # set the state of variable at `index` to `state`. self.var_state[index] = state self.varmodel.setData(self.varmodel.index(index), state, Qt.UserRole) def _clear(self): self.data = None self.varmodel[:] = [] self.var_state = {} self.saved_var_states = {} self.default_bbox.buttons[self.MDL - 1].setEnabled(True) self.bbox.buttons[self.MDL].setEnabled(True) def _update_points(self): """ Update the induced cut points. """ if self.data is None or not len(self.data): return def induce_cuts(method, data, var): dvar = _dispatch[type(method)](method, data, var) if dvar is None: # removed return [], None elif dvar is var: # no transformation took place return None, var elif is_discretized(dvar): return dvar.compute_value.points, dvar else: assert False for i, var in enumerate(self.varmodel): state = self.var_state[i] if state.points is None and state.disc_var is None: points, dvar = induce_cuts(state.method, self.data, var) new_state = state._replace(points=points, disc_var=dvar) self._set_var_state(i, new_state) def _method_index(self, method): return METHODS.index((type(method), )) def _current_default_method(self): method = self.default_method + 1 k = self.default_k if method == OWDiscretize.Leave: def_method = Leave() elif method == OWDiscretize.MDL: def_method = MDL() elif method == OWDiscretize.EqualFreq: def_method = EqualFreq(k) elif method == OWDiscretize.EqualWidth: def_method = EqualWidth(k) elif method == OWDiscretize.Remove: def_method = Remove() else: assert False return def_method def _current_method(self): if self.method == OWDiscretize.Default: method = Default(self._current_default_method()) elif self.method == OWDiscretize.Leave: method = Leave() elif self.method == OWDiscretize.MDL: method = MDL() elif self.method == OWDiscretize.EqualFreq: method = EqualFreq(self.k) elif self.method == OWDiscretize.EqualWidth: method = EqualWidth(self.k) elif self.method == OWDiscretize.Remove: method = Remove() elif self.method == OWDiscretize.Custom: method = Custom(self.cutpoints) else: assert False return method def _update_spin_positions(self): self.k_general.setDisabled(self.default_method not in [2, 3]) if self.default_method == 2: self.left.layout().insertWidget(1, self.k_general) elif self.default_method == 3: self.left.layout().insertWidget(2, self.k_general) self.k_specific.setDisabled(self.method not in [3, 4]) if self.method == 3: self.bbox.layout().insertWidget(4, self.k_specific) elif self.method == 4: self.bbox.layout().insertWidget(5, self.k_specific) def _default_disc_changed(self): self._update_spin_positions() method = self._current_default_method() state = DState(Default(method), None, None) for i, _ in enumerate(self.varmodel): if isinstance(self.var_state[i].method, Default): self._set_var_state(i, state) self._update_points() self.commit() def _disc_method_changed(self): self._update_spin_positions() indices = self.selected_indices() method = self._current_method() state = DState(method, None, None) for idx in indices: self._set_var_state(idx, state) self._update_points() self.commit() def _var_selection_changed(self, *args): indices = self.selected_indices() # set of all methods for the current selection methods = [self.var_state[i].method for i in indices] mset = set(methods) self.controlbox.setEnabled(len(mset) > 0) if len(mset) == 1: method = mset.pop() self.method = self._method_index(method) if isinstance(method, (EqualFreq, EqualWidth)): self.k = method.k elif isinstance(method, Custom): self.cutpoints = method.points else: # deselect the current button self.method = -1 bg = self.controlbox.group button_group_reset(bg) self._update_spin_positions() def selected_indices(self): rows = self.varview.selectionModel().selectedRows() return [index.row() for index in rows] def discretized_var(self, source): index = list(self.varmodel).index(source) state = self.var_state[index] if state.disc_var is None: return None elif state.disc_var is source: return source elif state.points == []: return None else: return state.disc_var def discretized_domain(self): """ Return the current effective discretized domain. """ if self.data is None: return None def disc_var(source): if source and source.is_continuous: return self.discretized_var(source) else: return source attributes = [disc_var(v) for v in self.data.domain.attributes] attributes = [v for v in attributes if v is not None] class_var = disc_var(self.data.domain.class_var) domain = Orange.data.Domain( attributes, class_var, metas=self.data.domain.metas ) return domain def commit(self): output = None if self.data is not None and len(self.data): domain = self.discretized_domain() output = self.data.from_table(domain, self.data) self.send("Data", output) def storeSpecificSettings(self): super().storeSpecificSettings() self.saved_var_states = { variable_key(var): self.var_state[i]._replace(points=None, disc_var=None) for i, var in enumerate(self.varmodel) } def send_report(self): self.report_items(( ("Default method", self.options[self.default_method + 1]),)) if self.varmodel: self.report_items("Thresholds", [ (var.name, DiscDelegate.cutsText(self.var_state[i]) or "leave numeric") for i, var in enumerate(self.varmodel)])
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 class Inputs: data = Input("Data", Orange.data.Table) learner = Input("Learner", Learner) class Outputs: data = Output("Data", Orange.data.Table) class Error(OWWidget.Error): imputation_failed = Msg("Imputation failed for '{}'") model_based_imputer_sparse = Msg("Model based imputer does not work for sparse data") settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(int(Method.Leave)) # type: int # Per-variable imputation state (synced in storeSpecificSettings) _variable_imputation_state = settings.ContextSetting({}) # type: VariableState autocommit = settings.Setting(True) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() self.data = None # type: Optional[Orange.data.Table] self.learner = None # type: Optional[Learner] self.default_learner = SimpleTreeLearner() self.modified = False self.executor = qconcurrent.ThreadExecutor(self) self.__task = None main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for method, _ in list(METHODS.items())[1:-1]: imputer = self.create_imputer(method) button = QRadioButton(imputer.name) button.setChecked(method == self.default_method_index) button_group.addButton(button, method) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView( selectionMode=QListView.ExtendedSelection, uniformItemSizes=True ) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed ) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for method in Method: imputer = self.create_imputer(method) button = QRadioButton(text=imputer.name) button_group.addButton(button, method) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected ) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, ) self.value_stack = value_stack = QStackedWidget() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addWidget(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection ) method_layout.addStretch(2) reset_button = QPushButton( "Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_state, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit( self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.button.setFixedWidth(180) box.layout().insertStretch(0) def create_imputer(self, method, *args): # type: (Method, ...) -> impute.BaseImputeMethod if method == Method.Model: if self.learner is not None: return impute.Model(self.learner) else: return impute.Model(self.default_learner) elif method == Method.AsAboveSoBelow: assert self.default_method_index != Method.AsAboveSoBelow default = self.create_imputer(Method(self.default_method_index)) m = AsDefault() m.method = default return m else: return METHODS[method](*args) @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: assert index != Method.AsAboveSoBelow self._default_method_index = index self.default_button_group.button(index).setChecked(True) # update variable view self.update_varview() self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @Inputs.data @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self._variable_imputation_state = {} # type: VariableState self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) # restore per variable imputation state self._restore_state(self._variable_imputation_state) self.update_varview() self.unconditional_commit() @Inputs.learner def set_learner(self, learner): self.learner = learner or self.default_learner imputer = self.create_imputer(Method.Model) button = self.default_button_group.button(Method.Model) button.setText(imputer.name) variable_button = self.variable_button_group.button(Method.Model) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = Method.Model self.update_varview() self.commit() def get_method_for_column(self, column_index): # type: (int) -> impute.BaseImputeMethod """ Return the imputation method for column by its index. """ assert 0 <= column_index < len(self.varmodel) idx = self.varmodel.index(column_index, 0) state = idx.data(StateRole) if state is None: state = (Method.AsAboveSoBelow, ()) return self.create_imputer(state[0], *state[1]) def _invalidate(self): self.modified = True if self.__task is not None: self.cancel() self.commit() def commit(self): self.cancel() self.warning() self.Error.imputation_failed.clear() self.Error.model_based_imputer_sparse.clear() if self.data is None or len(self.data) == 0 or len(self.varmodel) == 0: self.Outputs.data.send(self.data) self.modified = False return data = self.data impute_state = [ (i, var, self.get_method_for_column(i)) for i, var in enumerate(self.varmodel) ] # normalize to the effective method bypasing AsDefault impute_state = [ (i, var, m.method if isinstance(m, AsDefault) else m) for i, var, m in impute_state ] def impute_one(method, var, data): # type: (impute.BaseImputeMethod, Variable, Table) -> Any if isinstance(method, impute.Model) and data.is_sparse(): raise SparseNotSupported() elif isinstance(method, impute.DropInstances): return RowMask(method(data, var)) elif not method.supports_variable(var): raise VariableNotSupported(var) else: return method(data, var) futures = [] for _, var, method in impute_state: f = self.executor.submit( impute_one, copy.deepcopy(method), var, data) futures.append(f) w = qconcurrent.FutureSetWatcher(futures) w.doneAll.connect(self.__commit_finish) w.progressChanged.connect(self.__progress_changed) self.__task = Task(futures, w) self.progressBarInit(processEvents=False) self.setBlocking(True) @Slot() def __commit_finish(self): assert QThread.currentThread() is self.thread() assert self.__task is not None futures = self.__task.futures assert len(futures) == len(self.varmodel) assert self.data is not None self.__task = None self.setBlocking(False) self.progressBarFinished() data = self.data attributes = [] class_vars = [] drop_mask = np.zeros(len(self.data), bool) for i, (var, fut) in enumerate(zip(self.varmodel, futures)): assert fut.done() newvar = [] try: res = fut.result() except SparseNotSupported: self.Error.model_based_imputer_sparse() # ?? break except VariableNotSupported: self.warning("Default method can not handle '{}'". format(var.name)) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error for %s", var, exc_info=True) self.Error.imputation_failed(var.name) attributes = class_vars = None break else: if isinstance(res, RowMask): drop_mask |= res.mask newvar = var else: newvar = res if isinstance(newvar, Orange.data.Variable): newvar = [newvar] if i < len(data.domain.attributes): attributes.extend(newvar) else: class_vars.extend(newvar) if attributes is None: data = None else: domain = Orange.data.Domain(attributes, class_vars, data.domain.metas) try: data = self.data.from_table(domain, data[~drop_mask]) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error", exc_info=True) self.Error.imputation_failed("Unknown") data = None self.Outputs.data.send(data) self.modified = False @Slot(int, int) def __progress_changed(self, n, d): assert QThread.currentThread() is self.thread() assert self.__task is not None self.progressBarSet(100. * n / d) def cancel(self): if self.__task is not None: task, self.__task = self.__task, None task.cancel() task.watcher.doneAll.disconnect(self.__commit_finish) task.watcher.progressChanged.disconnect(self.__progress_changed) concurrent.futures.wait(task.futures) task.watcher.flush() self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.get_method_for_column(i) if not isinstance(method, AsDefault): specific.append("{} ({})".format(var.name, str(method))) default = self.create_imputer(Method.AsAboveSoBelow) if specific: self.report_items(( ("Default method", default.name), ("Specific imputers", ", ".join(specific)) )) else: self.report_items((("Method", default.name),)) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() defmethod = (Method.AsAboveSoBelow, ()) methods = [index.data(StateRole) for index in indexes] methods = [m if m is not None else defmethod for m in methods] methods = set(methods) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) fixed_value = None value_stack_enabled = False current_value_widget = None if len(methods) == 1: method_type, parameters = methods.pop() for m in Method: if method_type == m: self.variable_button_group.button(m).setChecked(True) if method_type == Method.Default: (fixed_value,) = parameters elif self.variable_button_group.checkedButton() is not None: # Uncheck the current button self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) assert self.variable_button_group.checkedButton() is None # Update variable methods GUI enabled state based on selection. for method in Method: # use a default constructed imputer to query support imputer = self.create_imputer(method) enabled = all(imputer.supports_variable(var) for var in selected_vars) button = self.variable_button_group.button(method) button.setEnabled(enabled) # Update the "Value" edit GUI. if not has_discrete: # no discrete variables -> allow mass edit for all (continuous vars) value_stack_enabled = True current_value_widget = self.value_double elif len(selected_vars) == 1: # single discrete var -> enable and fill the values combo value_stack_enabled = True current_value_widget = self.value_combo self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) else: # mixed type selection -> disable value_stack_enabled = False current_value_widget = None self.variable_button_group.button(Method.Default).setEnabled(False) self.value_stack.setEnabled(value_stack_enabled) if current_value_widget is not None: self.value_stack.setCurrentWidget(current_value_widget) if fixed_value is not None: # set current value if current_value_widget is self.value_combo: self.value_combo.setCurrentIndex(fixed_value) elif current_value_widget is self.value_double: self.value_double.setValue(fixed_value) else: assert False def set_method_for_current_selection(self, method_index): # type: (Method) -> None indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): # type: (List[QModelIndex], Method) -> None if method_index == Method.AsAboveSoBelow: for index in indexes: self.varmodel.setData(index, None, StateRole) elif method_index == Method.Default: current = self.value_stack.currentWidget() if current is self.value_combo: value = self.value_combo.currentIndex() else: value = self.value_double.value() for index in indexes: state = (int(Method.Default), (value,)) self.varmodel.setData(index, state, StateRole) else: state = (int(method_index), ()) for index in indexes: self.varmodel.setData(index, state, StateRole) self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData( index, self.get_method_for_column(index.row()), DisplayMethodRole) def _on_value_selected(self): # The fixed 'Value' in the widget has been changed by the user. self.variable_button_group.button(Method.Default).setChecked(True) self.set_method_for_current_selection(Method.Default) def reset_variable_state(self): indexes = list(map(self.varmodel.index, range(len(self.varmodel)))) self.set_method_for_indexes(indexes, Method.AsAboveSoBelow) self.variable_button_group.button(Method.AsAboveSoBelow).setChecked(True) def _store_state(self): # type: () -> VariableState """ Save the current variable imputation state """ state = {} # type: VariableState for i, var in enumerate(self.varmodel): index = self.varmodel.index(i) m = index.data(StateRole) if m is not None: state[var_key(var)] = m return state def _restore_state(self, state): # type: (VariableState) -> None """ Restore the variable imputation state from the saved state """ def check(state): # check if state is a proper State if isinstance(state, tuple) and len(state) == 2: m, p = state if isinstance(m, int) and isinstance(p, tuple) and \ 0 <= m < len(Method): return True return False for i, var in enumerate(self.varmodel): m = state.get(var_key(var), None) if check(m): self.varmodel.setData(self.varmodel.index(i), m, StateRole) def storeSpecificSettings(self): self._variable_imputation_state = self._store_state() super().storeSpecificSettings()
class SelectionSetsWidget(QFrame): """ Widget for managing multiple stored item selections """ selectionModified = Signal(bool) def __init__(self, parent): QFrame.__init__(self, parent) self.setContentsMargins(0, 0, 0, 0) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) self._setNameLineEdit = QLineEdit(self) layout.addWidget(self._setNameLineEdit) self._setListView = QListView(self) self._listModel = QStandardItemModel(self) self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSourceModel(self._listModel) self._setListView.setModel(self._proxyModel) self._setListView.setItemDelegate(ListItemDelegate(self)) self._setNameLineEdit.textChanged.connect( self._proxyModel.setFilterFixedString) self._completer = QCompleter(self._listModel, self) self._setNameLineEdit.setCompleter(self._completer) self._listModel.itemChanged.connect(self._onSetNameChange) layout.addWidget(self._setListView) buttonLayout = QHBoxLayout() self._addAction = QAction( "+", self, toolTip="Add a new sort key") self._updateAction = QAction( "Update", self, toolTip="Update/save current selection") self._removeAction = QAction( "\u2212", self, toolTip="Remove selected sort key.") self._addToolButton = QToolButton(self) self._updateToolButton = QToolButton(self) self._removeToolButton = QToolButton(self) self._updateToolButton.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self._addToolButton.setDefaultAction(self._addAction) self._updateToolButton.setDefaultAction(self._updateAction) self._removeToolButton.setDefaultAction(self._removeAction) buttonLayout.addWidget(self._addToolButton) buttonLayout.addWidget(self._updateToolButton) buttonLayout.addWidget(self._removeToolButton) layout.addLayout(buttonLayout) self.setLayout(layout) self._addAction.triggered.connect(self.addCurrentSelection) self._updateAction.triggered.connect(self.updateSelectedSelection) self._removeAction.triggered.connect(self.removeSelectedSelection) self._setListView.selectionModel().selectionChanged.connect( self._onListViewSelectionChanged) self.selectionModel = None self._selections = [] def sizeHint(self): size = QFrame.sizeHint(self) return QSize(size.width(), 150) def _onSelectionChanged(self, selected, deselected): self.setSelectionModified(True) def _onListViewSelectionChanged(self, selected, deselected): try: index = self._setListView.selectedIndexes()[0] except IndexError: return self.commitSelection(self._proxyModel.mapToSource(index).row()) def _onSetNameChange(self, item): self.selections[item.row()].name = str(item.text()) def _setButtonStates(self, val): self._updateToolButton.setEnabled(val) def setSelectionModel(self, selectionModel): if self.selectionModel: self.selectionModel.selectionChanged.disconnect( self._onSelectionChanged) self.selectionModel = selectionModel self.selectionModel.selectionChanged.connect(self._onSelectionChanged) def addCurrentSelection(self): item = self.addSelection( SelectionByKey(self.selectionModel.selection(), name="New selection", key=(1, 2, 3, 10))) index = self._proxyModel.mapFromSource(item.index()) self._setListView.setCurrentIndex(index) self._setListView.edit(index) self.setSelectionModified(False) def removeSelectedSelection(self): i = self._proxyModel.mapToSource(self._setListView.currentIndex()).row() self._listModel.takeRow(i) del self.selections[i] def updateCurentSelection(self): i = self._proxyModel.mapToSource(self._setListView.selectedIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def addSelection(self, selection, name=""): self._selections.append(selection) item = QStandardItem(selection.name) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listModel.appendRow(item) self.setSelectionModified(False) return item def updateSelectedSelection(self): i = self._proxyModel.mapToSource(self._setListView.currentIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def setSelectionModified(self, val): self._selectionModified = val self._setButtonStates(val) self.selectionModified.emit(bool(val)) def commitSelection(self, index): selection = self.selections[index] selection.select(self.selectionModel) def setSelections(self, selections): self._listModel.clear() for selection in selections: self.addSelection(selection) def selections(self): return self._selections selections = property(selections, setSelections)
class OWPythonScript(OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." category = "Transform" icon = "icons/PythonScript.svg" priority = 3150 keywords = ["program", "function"] class Inputs: data = MultiInput("Data", Table, replaces=["in_data"], default=True) learner = MultiInput("Learner", Learner, replaces=["in_learner"], default=True) classifier = MultiInput("Classifier", Model, replaces=["in_classifier"], default=True) object = MultiInput("Object", object, replaces=["in_object"], default=False, auto_summary=False) class Outputs: data = Output("Data", Table, replaces=["out_data"]) learner = Output("Learner", Learner, replaces=["out_learner"]) classifier = Output("Classifier", Model, replaces=["out_classifier"]) object = Output("Object", object, replaces=["out_object"], auto_summary=False) signal_names = ("data", "learner", "classifier", "object") settings_version = 2 scriptLibrary: 'List[_ScriptData]' = Setting([{ "name": "Table from numpy", "script": DEFAULT_SCRIPT, "filename": None }]) currentScriptIndex = Setting(0) scriptText: Optional[str] = Setting(None, schema_only=True) splitterState: Optional[bytes] = Setting(None) vimModeEnabled = Setting(False) class Error(OWWidget.Error): pass def __init__(self): super().__init__() for name in self.signal_names: setattr(self, name, []) self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) # Styling self.defaultFont = defaultFont = ( 'Menlo' if sys.platform == 'darwin' else 'Courier' if sys.platform in ['win32', 'cygwin'] else 'DejaVu Sans Mono') self.defaultFontSize = defaultFontSize = 13 self.editorBox = gui.vBox(self, box="Editor", spacing=4) self.splitCanvas.addWidget(self.editorBox) darkMode = QApplication.instance().property('darkMode') scheme_name = 'Dark' if darkMode else 'Light' syntax_highlighting_scheme = SYNTAX_HIGHLIGHTING_STYLES[scheme_name] self.pygments_style_class = make_pygments_style(scheme_name) eFont = QFont(defaultFont) eFont.setPointSize(defaultFontSize) # Fake Signature self.func_sig = func_sig = FunctionSignature( self.editorBox, syntax_highlighting_scheme, eFont) # Editor editor = PythonEditor(self) editor.setFont(eFont) editor.setup_completer_appearance((300, 180), eFont) # Fake return return_stmt = ReturnStatement(self.editorBox, syntax_highlighting_scheme, eFont) self.return_stmt = return_stmt # Match indentation textEditBox = QWidget(self.editorBox) textEditBox.setLayout(QHBoxLayout()) char_4_width = QFontMetrics(eFont).horizontalAdvance('0000') @editor.viewport_margins_updated.connect def _(width): func_sig.setIndent(width) textEditMargin = max(0, round(char_4_width - width)) return_stmt.setIndent(textEditMargin + width) textEditBox.layout().setContentsMargins(textEditMargin, 0, 0, 0) self.text = editor textEditBox.layout().addWidget(editor) self.editorBox.layout().addWidget(func_sig) self.editorBox.layout().addWidget(textEditBox) self.editorBox.layout().addWidget(return_stmt) self.editorBox.setAlignment(Qt.AlignVCenter) self.text.modificationChanged[bool].connect(self.onModificationChanged) # Controls self.editor_controls = gui.vBox(self.controlArea, box='Preferences') self.vim_box = gui.hBox(self.editor_controls, spacing=20) self.vim_indicator = VimIndicator(self.vim_box) vim_sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) vim_sp.setRetainSizeWhenHidden(True) self.vim_indicator.setSizePolicy(vim_sp) def enable_vim_mode(): editor.vimModeEnabled = self.vimModeEnabled self.vim_indicator.setVisible(self.vimModeEnabled) enable_vim_mode() gui.checkBox(self.vim_box, self, 'vimModeEnabled', 'Vim mode', tooltip="Only for the coolest.", callback=enable_vim_mode) self.vim_box.layout().addWidget(self.vim_indicator) @editor.vimModeIndicationChanged.connect def _(color, text): self.vim_indicator.indicator_color = color self.vim_indicator.indicator_text = text self.vim_indicator.update() # Library self.libraryListSource = [] self._cachedDocuments = {} self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) restore_saved = QAction("Undo Changes to Selected Script", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) restore_saved.triggered.connect(self.restoreSaved) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) menu.addAction(restore_saved) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.button(self.buttonsArea, self, 'Run', callback=self.commit) self.run_action = QAction("Run script", self, triggered=self.commit, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R)) self.addAction(self.run_action) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self.splitCanvas, 'Console') self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.splitCanvas.setSizes([2, 1]) self.controlArea.layout().addStretch(10) self._restoreState() self.settingsAboutToBePacked.connect(self._saveState) def sizeHint(self) -> QSize: return super().sizeHint().expandedTo(QSize(800, 600)) def _restoreState(self): self.libraryListSource = [ Script.fromdict(s) for s in self.scriptLibrary ] self.libraryList.wrap(self.libraryListSource) select_row(self.libraryView, self.currentScriptIndex) if self.scriptText is not None: current = self.text.toPlainText() # do not mark scripts as modified if self.scriptText != current: self.text.document().setPlainText(self.scriptText) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) def _saveState(self): self.scriptLibrary = [s.asdict() for s in self.libraryListSource] self.scriptText = self.text.toPlainText() self.splitterState = bytes(self.splitCanvas.saveState()) def set_input(self, index, obj, signal): dic = getattr(self, signal) dic[index] = obj def insert_input(self, index, obj, signal): dic = getattr(self, signal) dic.insert(index, obj) def remove_input(self, index, signal): dic = getattr(self, signal) dic.pop(index) @Inputs.data def set_data(self, index, data): self.set_input(index, data, "data") @Inputs.data.insert def insert_data(self, index, data): self.insert_input(index, data, "data") @Inputs.data.remove def remove_data(self, index): self.remove_input(index, "data") @Inputs.learner def set_learner(self, index, learner): self.set_input(index, learner, "learner") @Inputs.learner.insert def insert_learner(self, index, learner): self.insert_input(index, learner, "learner") @Inputs.learner.remove def remove_learner(self, index): self.remove_input(index, "learner") @Inputs.classifier def set_classifier(self, index, classifier): self.set_input(index, classifier, "classifier") @Inputs.classifier.insert def insert_classifier(self, index, classifier): self.insert_input(index, classifier, "classifier") @Inputs.classifier.remove def remove_classifier(self, index): self.remove_input(index, "classifier") @Inputs.object def set_object(self, index, object): self.set_input(index, object, "object") @Inputs.object.insert def insert_object(self, index, object): self.insert_input(index, object, "object") @Inputs.object.remove def remove_object(self, index): self.remove_input(index, "object") def handleNewSignals(self): # update fake signature labels self.func_sig.update_signal_text( {n: len(getattr(self, n)) for n in self.signal_names}) self.commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *_): self.libraryList.append( Script("New script", self.text.toPlainText(), 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *_): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)') if filename: name = os.path.basename(filename) with tokenize.open(filename) as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *_): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *_): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, _deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if not isinstance(script, Script): script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PygmentsHighlighter(doc) doc.highlighter.set_style(self.pygments_style_class) doc.setDefaultFont( QFont(self.defaultFont, pointSize=self.defaultFontSize)) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *_): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def restoreSaved(self): index = self.selectedScriptIndex() if index is not None: self.text.document().setPlainText(self.libraryList[index].script) self.text.document().setModified(False) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)') if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = {} for name in self.signal_names: value = getattr(self, name) all_values = list(value) one_value = all_values[0] if len(all_values) == 1 else None d["in_" + name + "s"] = all_values d["in_" + name] = one_value return d def commit(self): self.Error.clear() lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for signal in self.signal_names: out_var = self.console.locals.get("out_" + signal) signal_type = getattr(self.Outputs, signal).type if not isinstance(out_var, signal_type) and out_var is not None: self.Error.add_message( signal, "'{}' has to be an instance of '{}'.".format( signal, signal_type.__name__)) getattr(self.Error, signal)() out_var = None getattr(self.Outputs, signal).send(out_var) def keyPressEvent(self, e): if e.matches(QKeySequence.InsertLineSeparator): # run on Shift+Enter, Ctrl+Enter self.run_action.trigger() e.accept() else: super().keyPressEvent(e) def dragEnterEvent(self, event): # pylint: disable=no-self-use urls = event.mimeData().urls() if urls: # try reading the file as text c = read_file_content(urls[0].toLocalFile(), limit=1000) if c is not None: event.acceptProposedAction() @classmethod def migrate_settings(cls, settings, version): if version is not None and version < 2: scripts = settings.pop("libraryListSource") # type: List[Script] library = [ dict(name=s.name, script=s.script, filename=s.filename) for s in scripts ] # type: List[_ScriptData] settings["scriptLibrary"] = library def onDeleteWidget(self): self.text.terminate() super().onDeleteWidget()
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 inputs = [("Data", Orange.data.Table, "set_data"), ("Learner", Learner, "set_learner")] outputs = [("Data", Orange.data.Table)] class Error(OWWidget.Error): imputation_failed = Msg("Imputation failed for '{}'") DEFAULT_LEARNER = SimpleTreeLearner() METHODS = [ AsDefault(), impute.DoNotImpute(), impute.Average(), impute.AsValue(), impute.Model(DEFAULT_LEARNER), impute.Random(), impute.DropInstances(), impute.Default() ] DEFAULT, DO_NOT_IMPUTE, MODEL_BASED_IMPUTER, AS_INPUT = 0, 1, 4, 7 settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(DO_NOT_IMPUTE) variable_methods = settings.ContextSetting({}) autocommit = settings.Setting(True) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() # copy METHODS (some are modified by the widget) self.methods = copy.deepcopy(OWImpute.METHODS) main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for i, method in enumerate(self.methods): if not method.columns_only: button = QRadioButton(method.name) button.setChecked(i == self.default_method_index) button_group.addButton(button, i) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView(selectionMode=QListView.ExtendedSelection) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for i, method in enumerate(self.methods): button = QRadioButton(text=method.name) button_group.addButton(button, i) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, ) self.value_stack = value_stack = QStackedWidget() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addWidget(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection) method_layout.addStretch(2) reset_button = QPushButton("Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_methods, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit(self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 80) box.layout().insertWidget(0, self.report_button) self.data = None self.learner = None self.modified = False self.default_method = self.methods[self.default_method_index] @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: self._default_method_index = index self.default_button_group.button(index).setChecked(True) self.default_method = self.methods[self.default_method_index] self.methods[self.DEFAULT].method = self.default_method # update variable view for index in map(self.varmodel.index, range(len(self.varmodel))): method = self.variable_methods.get(index.row(), self.methods[self.DEFAULT]) self.varmodel.setData(index, method, Qt.UserRole) self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self.variable_methods = {} self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) self.update_varview() self.unconditional_commit() def set_learner(self, learner): self.learner = learner or self.DEFAULT_LEARNER imputer = self.methods[self.MODEL_BASED_IMPUTER] imputer.learner = self.learner button = self.default_button_group.button(self.MODEL_BASED_IMPUTER) button.setText(imputer.name) variable_button = self.variable_button_group.button( self.MODEL_BASED_IMPUTER) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = self.MODEL_BASED_IMPUTER self.update_varview() self.commit() def get_method_for_column(self, column_index): """Returns the imputation method for column by its index. """ if not isinstance(column_index, int): column_index = column_index.row() return self.variable_methods.get(column_index, self.methods[self.DEFAULT]) def _invalidate(self): self.modified = True self.commit() def commit(self): data = self.data if self.data is not None: if not len(self.data): self.send("Data", self.data) self.modified = False return drop_mask = np.zeros(len(self.data), bool) attributes = [] class_vars = [] self.warning() self.Error.imputation_failed.clear() with self.progressBar(len(self.varmodel)) as progress: for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, self.default_method) try: if not method.supports_variable(var): self.warning( "Default method can not handle '{}'".format( var.name)) elif isinstance(method, impute.DropInstances): drop_mask |= method(self.data, var) else: var = method(self.data, var) except Exception: # pylint: disable=broad-except self.Error.imputation_failed(var.name) attributes = class_vars = None break if isinstance(var, Orange.data.Variable): var = [var] if i < len(self.data.domain.attributes): attributes.extend(var) else: class_vars.extend(var) progress.advance() if attributes is None: data = None else: domain = Orange.data.Domain(attributes, class_vars, self.data.domain.metas) data = self.data.from_table(domain, self.data[~drop_mask]) self.send("Data", data) self.modified = False def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, None) if method is not None: specific.append("{} ({})".format(var.name, str(method))) default = self.default_method.name if specific: self.report_items((("Default method", default), ("Specific imputers", ", ".join(specific)))) else: self.report_items((("Method", default), )) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() methods = [self.get_method_for_column(i.row()) for i in indexes] def method_key(method): """ Decompose method into its type and parameters. """ # The return value should be hashable and __eq__ comparable if isinstance(method, AsDefault): return AsDefault, (method.method, ) elif isinstance(method, impute.Model): return impute.Model, (method.learner, ) elif isinstance(method, impute.Default): return impute.Default, (method.default, ) else: return type(method), None methods = set(method_key(m) for m in methods) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) fixed_value = None value_stack_enabled = False current_value_widget = None if len(methods) == 1: method_type, parameters = methods.pop() for i, m in enumerate(self.methods): if method_type == type(m): self.variable_button_group.button(i).setChecked(True) if method_type is impute.Default: (fixed_value, ) = parameters elif self.variable_button_group.checkedButton() is not None: # Uncheck the current button self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) assert self.variable_button_group.checkedButton() is None for method, button in zip(self.methods, self.variable_button_group.buttons()): enabled = all( method.supports_variable(var) for var in selected_vars) button.setEnabled(enabled) if not has_discrete: value_stack_enabled = True current_value_widget = self.value_double elif len(selected_vars) == 1: value_stack_enabled = True current_value_widget = self.value_combo self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) else: value_stack_enabled = False current_value_widget = None self.variable_button_group.button(self.AS_INPUT).setEnabled(False) self.value_stack.setEnabled(value_stack_enabled) if current_value_widget is not None: self.value_stack.setCurrentWidget(current_value_widget) if fixed_value is not None: if current_value_widget is self.value_combo: self.value_combo.setCurrentIndex(fixed_value) elif current_value_widget is self.value_double: self.value_double.setValue(fixed_value) else: assert False def set_method_for_current_selection(self, method_index): indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): if method_index == self.DEFAULT: for index in indexes: self.variable_methods.pop(index.row(), None) elif method_index == OWImpute.AS_INPUT: current = self.value_stack.currentWidget() if current is self.value_combo: value = self.value_combo.currentIndex() else: value = self.value_double.value() for index in indexes: method = impute.Default(default=value) self.variable_methods[index.row()] = method else: method = self.methods[method_index].copy() for index in indexes: self.variable_methods[index.row()] = method self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), Qt.UserRole) def _on_value_selected(self): # The fixed 'Value' in the widget has been changed by the user. self.variable_button_group.button(self.AS_INPUT).setChecked(True) self.set_method_for_current_selection(self.AS_INPUT) def reset_variable_methods(self): indexes = list(map(self.varmodel.index, range(len(self.varmodel)))) self.set_method_for_indexes(indexes, self.DEFAULT) self.variable_button_group.button(self.DEFAULT).setChecked(True)
class OWDiscretize(widget.OWWidget): # pylint: disable=too-many-instance-attributes name = "Discretize" description = "Discretize the numeric data features." icon = "icons/Discretize.svg" keywords = [] class Inputs: data = Input("Data", Orange.data.Table, doc="Input data table") class Outputs: data = Output("Data", Orange.data.Table, doc="Table with discretized features") settingsHandler = settings.DomainContextHandler() settings_version = 2 saved_var_states = settings.ContextSetting({}) #: The default method name default_method_name = settings.Setting(Methods.EqualFreq.name) #: The k for Equal{Freq,Width} default_k = settings.Setting(3) #: The default cut points for custom entry default_cutpoints: Tuple[float, ...] = settings.Setting(()) autosend = settings.Setting(True) #: Discretization methods Default, Leave, MDL, EqualFreq, EqualWidth, Remove, Custom = list(Methods) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() #: input data self.data = None self.class_var = None #: Current variable discretization state self.var_state = {} #: Saved variable discretization settings (context setting) self.saved_var_states = {} self.method = Methods.Default self.k = 5 self.cutpoints = () self.default_cutpoints = () box = gui.vBox(self.controlArea, self.tr("Default Discretization")) self._default_method_ = 0 self.default_bbox = rbox = gui.radioButtons( box, self, "_default_method_", callback=self._default_disc_changed) self.default_button_group = bg = rbox.findChild(QButtonGroup) bg.buttonClicked[int].connect(self.set_default_method) rb = gui.hBox(rbox) self.left = gui.vBox(rb) right = gui.vBox(rb) rb.layout().setStretch(0, 1) rb.layout().setStretch(1, 1) self.options = [ (Methods.Default, self.tr("Default")), (Methods.Leave, self.tr("Leave numeric")), (Methods.MDL, self.tr("Entropy-MDL discretization")), (Methods.EqualFreq, self.tr("Equal-frequency discretization")), (Methods.EqualWidth, self.tr("Equal-width discretization")), (Methods.Remove, self.tr("Remove numeric variables")), (Methods.Custom, self.tr("Manual")), ] for id_, opt in self.options[1:]: t = gui.appendRadioButton(rbox, opt) bg.setId(t, id_) t.setChecked(id_ == self.default_method) [right, self.left][opt.startswith("Equal")].layout().addWidget(t) def _intbox(parent, attr, callback): box = gui.indentedBox(parent) s = gui.spin(box, self, attr, minv=2, maxv=10, label="Num. of intervals:", callback=callback) s.setMaximumWidth(60) s.setAlignment(Qt.AlignRight) gui.rubber(s.box) sp = box.sizePolicy() sp.setControlType(sp.SpinBox) return box.box self.k_general = _intbox(self.left, "default_k", self._default_disc_changed) self.k_general.layout().setContentsMargins(0, 0, 0, 0) def manual_cut_editline(text="") -> QLineEdit: edit = QLineEdit( text=text, placeholderText="e.g. 0.0, 0.5, 1.0", toolTip="Enter fixed discretization cut points (a comma " "separated list of strictly increasing numbers e.g. " "0.0, 0.5, 1.0).", ) @edit.textChanged.connect def update(): validator = edit.validator() if validator is not None: state, _, _ = validator.validate(edit.text(), 0) else: state = QValidator.Acceptable palette = edit.palette() colors = { QValidator.Intermediate: (Qt.yellow, Qt.black), QValidator.Invalid: (Qt.red, Qt.black), }.get(state, None) if colors is None: palette = QPalette() else: palette.setColor(QPalette.Base, colors[0]) palette.setColor(QPalette.Text, colors[1]) cr = edit.cursorRect() p = edit.mapToGlobal(cr.bottomRight()) edit.setPalette(palette) if state != QValidator.Acceptable and edit.isVisible(): show_tip(edit, p, edit.toolTip(), textFormat=Qt.RichText) else: show_tip(edit, p, "") return edit self.manual_cuts_edit = manual_cut_editline( text=", ".join(map(str, self.default_cutpoints))) def set_manual_default_cuts(): text = self.manual_cuts_edit.text() self.default_cutpoints = tuple( float(s.strip()) for s in text.split(",") if s.strip()) self._default_disc_changed() self.manual_cuts_edit.editingFinished.connect(set_manual_default_cuts) validator = IncreasingNumbersListValidator() self.manual_cuts_edit.setValidator(validator) ibox = gui.indentedBox(right, orientation=Qt.Horizontal) ibox.layout().addWidget(self.manual_cuts_edit) right.layout().addStretch(10) self.left.layout().addStretch(10) self.connect_control( "default_cutpoints", lambda values: self.manual_cuts_edit.setText(", ".join( map(str, values)))) vlayout = QHBoxLayout() box = gui.widgetBox(self.controlArea, "Individual Attribute Settings", orientation=vlayout, spacing=8) # List view with all attributes self.varview = QListView( selectionMode=QListView.ExtendedSelection, uniformItemSizes=True, ) self.varview.setItemDelegate(DiscDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._var_selection_changed) vlayout.addWidget(self.varview) # Controls for individual attr settings self.bbox = controlbox = gui.radioButtons( box, self, "method", callback=self._disc_method_changed) vlayout.addWidget(controlbox) self.variable_button_group = bg = controlbox.findChild(QButtonGroup) for id_, opt in self.options[:5]: b = gui.appendRadioButton(controlbox, opt) bg.setId(b, id_) self.k_specific = _intbox(controlbox, "k", self._disc_method_changed) gui.appendRadioButton(controlbox, "Remove attribute", id=Methods.Remove) b = gui.appendRadioButton(controlbox, "Manual", id=Methods.Custom) self.manual_cuts_specific = manual_cut_editline() self.manual_cuts_specific.setValidator(validator) b.toggled[bool].connect(self.manual_cuts_specific.setEnabled) def set_manual_cuts(): text = self.manual_cuts_specific.text() points = [t for t in text.split(",") if t.split()] self.cutpoints = tuple(float(t) for t in points) self._disc_method_changed() self.manual_cuts_specific.editingFinished.connect(set_manual_cuts) self.connect_control( "cutpoints", lambda values: self.manual_cuts_specific.setText(", ".join( map(str, values)))) ibox = gui.indentedBox(controlbox, orientation=Qt.Horizontal) self.copy_current_to_manual_button = b = FixedSizeButton( text="CC", toolTip="Copy the current cut points to manual mode", enabled=False) b.clicked.connect(self._copy_to_manual) ibox.layout().addWidget(self.manual_cuts_specific) ibox.layout().addWidget(b) gui.rubber(controlbox) controlbox.setEnabled(False) bg.button(self.method) self.controlbox = controlbox box = gui.auto_apply(self.controlArea, self, "autosend") box.button.setFixedWidth(180) box.layout().insertStretch(0) self._update_spin_positions() self.info.set_input_summary(self.info.NoInput) self.info.set_output_summary(self.info.NoOutput) @property def default_method(self) -> Methods: return Methods[self.default_method_name] @default_method.setter def default_method(self, method): self.set_default_method(method) def set_default_method(self, method: Methods): if isinstance(method, int): method = Methods(method) else: method = Methods.from_method(method) if method != self.default_method: self.default_method_name = method.name self.default_button_group.button(method).setChecked(True) self._default_disc_changed() @Inputs.data def set_data(self, data): self.closeContext() self.data = data if self.data is not None: self._initialize(data) self.openContext(data) # Restore the per variable discretization settings self._restore(self.saved_var_states) # Complete the induction of cut points self._update_points() self.info.set_input_summary(len(data), format_summary_details(data)) else: self.info.set_input_summary(self.info.NoInput) self._clear() self.unconditional_commit() def _initialize(self, data): # Initialize the default variable states for new data. self.class_var = data.domain.class_var cvars = [var for var in data.domain.variables if var.is_continuous] self.varmodel[:] = cvars has_disc_class = data.domain.has_discrete_class def set_enabled(box: QWidget, id_: Methods, state: bool): bg = box.findChild(QButtonGroup) b = bg.button(id_) b.setEnabled(state) set_enabled(self.default_bbox, self.MDL, has_disc_class) bg = self.bbox.findChild(QButtonGroup) b = bg.button(Methods.MDL) b.setEnabled(has_disc_class) set_enabled(self.bbox, self.MDL, has_disc_class) # If the newly disabled MDL button is checked then change it if not has_disc_class and self.default_method == self.MDL: self.default_method = Methods.Leave if not has_disc_class and self.method == self.MDL: self.method = Methods.Default # Reset (initialize) the variable discretization states. self._reset() def _restore(self, saved_state): # Restore variable states from a saved_state dictionary. def_method = self._current_default_method() for i, var in enumerate(self.varmodel): key = variable_key(var) if key in saved_state: state = saved_state[key] if isinstance(state.method, Default): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _reset(self): # restore the individual variable settings back to defaults. def_method = self._current_default_method() self.var_state = {} for i in range(len(self.varmodel)): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _set_var_state(self, index, state): # set the state of variable at `index` to `state`. self.var_state[index] = state self.varmodel.setData(self.varmodel.index(index), state, Qt.UserRole) def _clear(self): self.data = None self.varmodel[:] = [] self.var_state = {} self.saved_var_states = {} self.default_button_group.button(self.MDL).setEnabled(True) self.variable_button_group.button(self.MDL).setEnabled(True) def _update_points(self): """ Update the induced cut points. """ if self.data is None: return def induce_cuts(method, data, var): dvar = _dispatch[type(method)](method, data, var) if dvar is None: # removed return [], None elif dvar is var: # no transformation took place return None, var elif is_discretized(dvar): return dvar.compute_value.points, dvar raise ValueError for i, var in enumerate(self.varmodel): state = self.var_state[i] if state.points is None and state.disc_var is None: points, dvar = induce_cuts(state.method, self.data, var) new_state = state._replace(points=points, disc_var=dvar) self._set_var_state(i, new_state) def _current_default_method(self): method = self.default_method k = self.default_k if method == Methods.Leave: def_method = Leave() elif method == Methods.MDL: def_method = MDL() elif method == Methods.EqualFreq: def_method = EqualFreq(k) elif method == Methods.EqualWidth: def_method = EqualWidth(k) elif method == Methods.Remove: def_method = Remove() elif method == Methods.Custom: def_method = Custom(self.default_cutpoints) else: assert False return def_method def _current_method(self): if self.method == Methods.Default: method = Default(self._current_default_method()) elif self.method == Methods.Leave: method = Leave() elif self.method == Methods.MDL: method = MDL() elif self.method == Methods.EqualFreq: method = EqualFreq(self.k) elif self.method == Methods.EqualWidth: method = EqualWidth(self.k) elif self.method == Methods.Remove: method = Remove() elif self.method == Methods.Custom: method = Custom(self.cutpoints) else: assert False return method def _update_spin_positions(self): kmethods = [Methods.EqualFreq, Methods.EqualWidth] self.k_general.setDisabled(self.default_method not in kmethods) if self.default_method == Methods.EqualFreq: self.left.layout().insertWidget(1, self.k_general) elif self.default_method == Methods.EqualWidth: self.left.layout().insertWidget(2, self.k_general) self.k_specific.setDisabled(self.method not in kmethods) if self.method == Methods.EqualFreq: self.bbox.layout().insertWidget(4, self.k_specific) elif self.method == Methods.EqualWidth: self.bbox.layout().insertWidget(5, self.k_specific) def _default_disc_changed(self): self._update_spin_positions() method = self._current_default_method() state = DState(Default(method), None, None) for i, _ in enumerate(self.varmodel): if isinstance(self.var_state[i].method, Default): self._set_var_state(i, state) self._update_points() self.commit() def _disc_method_changed(self): self._update_spin_positions() indices = self.selected_indices() method = self._current_method() state = DState(method, None, None) for idx in indices: self._set_var_state(idx, state) self._update_points() self._copy_to_manual_update_enabled() self.commit() def _copy_to_manual(self): indices = self.selected_indices() # set of all methods for the current selection if len(indices) != 1: return index = indices[0] state = self.var_state[index] var = self.varmodel[index] fmt = var.repr_val points = state.points if points is None: points = () else: points = tuple(state.points) state = state._replace(method=Custom(points), points=None, disc_var=None) self._set_var_state(index, state) self.method = Methods.Custom self.cutpoints = points self.manual_cuts_specific.setText(", ".join(map(fmt, points))) self._update_points() self.commit() def _copy_to_manual_update_enabled(self): indices = self.selected_indices() methods = [self.var_state[i].method for i in indices] self.copy_current_to_manual_button.setEnabled( len(indices) == 1 and not isinstance(methods[0], Custom)) def _var_selection_changed(self, *_): self._copy_to_manual_update_enabled() indices = self.selected_indices() # set of all methods for the current selection methods = [self.var_state[i].method for i in indices] def key(method): if isinstance(method, Default): return Default, (None, ) return type(method), tuple(method) mset = list(unique_everseen(methods, key=key)) self.controlbox.setEnabled(len(mset) > 0) if len(mset) == 1: method = mset.pop() self.method = Methods.from_method(method) if isinstance(method, (EqualFreq, EqualWidth)): self.k = method.k elif isinstance(method, Custom): self.cutpoints = method.points else: # deselect the current button self.method = -1 bg = self.controlbox.group button_group_reset(bg) self._update_spin_positions() def selected_indices(self): rows = self.varview.selectionModel().selectedRows() return [index.row() for index in rows] def method_for_index(self, index): state = self.var_state[index] return state.method def discretized_var(self, index): # type: (int) -> Optional[Orange.data.DiscreteVariable] state = self.var_state[index] if state.disc_var is not None and state.points == []: # Removed by MDL Entropy return None else: return state.disc_var def discretized_domain(self): """ Return the current effective discretized domain. """ if self.data is None: return None # a mapping of all applied changes for variables in `varmodel` mapping = { var: self.discretized_var(i) for i, var in enumerate(self.varmodel) } def disc_var(source): return mapping.get(source, source) # map the full input domain to the new variables (where applicable) attributes = [disc_var(v) for v in self.data.domain.attributes] attributes = [v for v in attributes if v is not None] class_vars = [disc_var(v) for v in self.data.domain.class_vars] class_vars = [v for v in class_vars if v is not None] domain = Orange.data.Domain(attributes, class_vars, metas=self.data.domain.metas) return domain def commit(self): output = None if self.data is not None: domain = self.discretized_domain() output = self.data.transform(domain) summary = len(output) if output else self.info.NoOutput details = format_summary_details(output) if output else "" self.info.set_output_summary(summary, details) self.Outputs.data.send(output) def storeSpecificSettings(self): super().storeSpecificSettings() self.saved_var_states = { variable_key(var): self.var_state[i]._replace(points=None, disc_var=None) for i, var in enumerate(self.varmodel) } def send_report(self): self.report_items( (("Default method", self.options[self.default_method][1]), )) if self.varmodel: self.report_items( "Thresholds", [(var.name, DiscDelegate.cutsText(self.var_state[i], var.repr_val) or "leave numeric") for i, var in enumerate(self.varmodel)]) @classmethod def migrate_settings(cls, settings, version): # pylint: disable=redefined-outer-name if version is None or version < 2: # was stored as int indexing Methods (but offset by 1) default = settings.pop("default_method", 0) default = Methods(default + 1) settings["default_method_name"] = default.name
class OWFeatureConstructor(OWWidget): name = "特征构造器(Feature Constructor)" description = "用输入数据集中的现有特征构造新特征。" icon = "icons/FeatureConstructor.svg" keywords = ['tezheng', 'gouzao', 'tezhenggouzao'] category = "数据(Data)" icon = "icons/FeatureConstructor.svg" class Inputs: data = Input("数据(Data)", Orange.data.Table, replaces=['Data']) class Outputs: data = Output("数据(Data)", Orange.data.Table, replaces=['Data']) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) expressions_with_values = ContextSetting(False) settings_version = 2 EDITORS = [(ContinuousDescriptor, ContinuousFeatureEditor), (DateTimeDescriptor, DateTimeFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor)] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") class Warning(OWWidget.Warning): renamed_var = Msg("Recently added variable has been renamed, " "to avoid duplicates.\n") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "变量定义") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget(sizePolicy=QSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton("新建", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def generate_newname(fmt): return unique_name(fmt, self.reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("数值数据") cont.triggered.connect(lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3))) disc = menu.addAction("分类数据") disc.triggered.connect(lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", (), False))) string = menu.addAction("文本") string.triggered.connect(lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), ""))) datetime = menu.addAction("日期/时间") datetime.triggered.connect(lambda: self.addFeature( DateTimeDescriptor(generate_newname("T{}"), ""))) menu.addSeparator() self.duplicateaction = menu.addAction("复制选中变量") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton("删除", toolTip="删除选中变量", minimumWidth=120, shortcut=QKeySequence.Delete) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView(minimumWidth=200, minimumHeight=50, sizePolicy=QSizePolicy( QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) self.fix_button = gui.button(self.buttonsArea, self, "Upgrade Expressions", callback=self.fix_expressions) self.fix_button.setHidden(True) gui.button(self.buttonsArea, self, "Send", callback=self.apply, default=True) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: self.Warning.clear() editor = self.editorstack.currentWidget() proposed = editor.editorData().name uniq = get_unique_names(self.reserved_names(self.currentIndex), proposed) feature = editor.editorData() if editor.editorData().name != uniq: self.Warning.renamed_var() feature = feature.__class__(uniq, *feature[1:]) self.featuremodel[self.currentIndex] = feature self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) def reserved_names(self, idx_=None): varnames = [] if self.data is not None: varnames = [ var.name for var in self.data.domain.variables + self.data.domain.metas ] varnames += [ desc.name for idx, desc in enumerate(self.featuremodel) if idx != idx_ ] return set(varnames) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data self.expressions_with_values = False self.descriptors = [] self.currentIndex = -1 if self.data is not None: self.openContext(data) # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect(self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect(self._on_selectedVariableChanged) self.fix_button.setHidden(not self.expressions_with_values) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.Outputs.data.send(None) self.fix_button.setHidden(True) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and self.featuremodel.rowCount(): # Deleting the last item clears selection self.setCurrentIndex(self.featuremodel.rowCount() - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) @staticmethod def check_attrs_values(attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) # ast.parse can return arbitrary errors, not only SyntaxError # pylint: disable=broad-except except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): def report_error(err): log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) try: new_variables = construct_variables(desc, self.data, self.expressions_with_values) # user's expression can contain arbitrary errors except Exception as err: # pylint: disable=broad-except report_error(err) return attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas)) try: for variable in new_variables: variable.compute_value.mask_exceptions = False data = self.data.transform(new_domain) # user's expression can contain arbitrary errors # pylint: disable=broad-except except Exception as err: report_error(err) return finally: for variable in new_variables: variable.compute_value.mask_exceptions = True disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): desc = "categorical" if feature.values: desc += " with values " \ + ", ".join(f"'{val}'" for val in feature.values) if feature.ordered: desc += "; ordered" elif isinstance(feature, ContinuousDescriptor): desc = "numeric" elif isinstance(feature, DateTimeDescriptor): desc = "date/time" else: desc = "text" items[feature.name] = f"{feature.expression} ({desc})" self.report_items(report.plural("Constructed feature{s}", len(items)), items) def fix_expressions(self): dlg = QMessageBox( QMessageBox.Question, "Fix Expressions", "This widget's behaviour has changed. Values of categorical " "variables are now inserted as their textual representations " "(strings); previously they appeared as integer numbers, with an " "attribute '.value' that contained the text.\n\n" "The widget currently runs in compatibility mode. After " "expressions are updated, manually check for their correctness.") dlg.addButton("Update", QMessageBox.ApplyRole) dlg.addButton("Cancel", QMessageBox.RejectRole) if dlg.exec() == QMessageBox.RejectRole: return def fixer(mo): var = domain[mo.group(2)] if mo.group(3) == ".value": # uses string; remove `.value` return "".join(mo.group(1, 2, 4)) # Uses ints: get them by indexing return mo.group(1) + "{" + \ ", ".join(f"'{val}': {i}" for i, val in enumerate(var.values)) + \ f"}}[{var.name}]" + mo.group(4) domain = self.data.domain disc_vars = "|".join(f"{var.name}" for var in chain(domain.variables, domain.metas) if var.is_discrete) expr = re.compile(r"(^|\W)(" + disc_vars + r")(\.value)?(\W|$)") self.descriptors[:] = [ descriptor._replace( expression=expr.sub(fixer, descriptor.expression)) for descriptor in self.descriptors ] self.expressions_with_values = False self.fix_button.hide() index = self.currentIndex self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(index) self.apply() @classmethod def migrate_context(cls, context, version): if version is None or version < 2: used_vars = set( chain(*( freevars(ast.parse(descriptor.expression, mode="eval"), []) for descriptor in context.values["descriptors"] if descriptor.expression))) disc_vars = { name for (name, vtype) in chain(context.attributes.items(), context.metas.items()) if vtype == 1 } if used_vars & disc_vars: context.values["expressions_with_values"] = True
class DiscreteVariableEditor(VariableEditor): """An editor widget for editing a discrete variable. Extends the :class:`VariableEditor` to enable editing of variables values. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) form = self.layout().itemAt(0) assert isinstance(form, QFormLayout) #: A list model of discrete variable's values. self.values_model = itemmodels.PyListModel( flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable ) vlayout = QVBoxLayout(spacing=1, margin=0) self.values_edit = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed ) self.values_edit.setItemDelegate(CategoriesEditDelegate(self)) self.values_edit.setModel(self.values_model) self.values_model.dataChanged.connect(self.on_values_changed) self.values_edit.selectionModel().selectionChanged.connect( self.on_value_selection_changed) self.values_model.layoutChanged.connect(self.on_value_selection_changed) self.values_model.rowsMoved.connect(self.on_value_selection_changed) vlayout.addWidget(self.values_edit) hlayout = QHBoxLayout(spacing=1, margin=0) self.categories_action_group = group = QActionGroup( self, objectName="action-group-categories", enabled=False ) self.move_value_up = QAction( "\N{UPWARDS ARROW}", group, toolTip="向上移动所选项。", shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_BracketLeft), shortcutContext=Qt.WidgetShortcut, ) self.move_value_up.triggered.connect(self.move_up) self.move_value_down = QAction( "\N{DOWNWARDS ARROW}", group, toolTip="向下移动所选项。", shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_BracketRight), shortcutContext=Qt.WidgetShortcut, ) self.move_value_down.triggered.connect(self.move_down) self.add_new_item = QAction( "+", group, objectName="action-add-item", toolTip="增加一个新的项。", shortcut=QKeySequence(QKeySequence.New), shortcutContext=Qt.WidgetShortcut, ) self.remove_item = QAction( "\N{MINUS SIGN}", group, objectName="action-remove-item", toolTip="删除选中项。", shortcut=QKeySequence(QKeySequence.Delete), shortcutContext=Qt.WidgetShortcut, ) self.add_new_item.triggered.connect(self._add_category) self.remove_item.triggered.connect(self._remove_category) button1 = FixedSizeButton( self, defaultAction=self.move_value_up, accessibleName="Move up" ) button2 = FixedSizeButton( self, defaultAction=self.move_value_down, accessibleName="Move down" ) button3 = FixedSizeButton( self, defaultAction=self.add_new_item, accessibleName="Add" ) button4 = FixedSizeButton( self, defaultAction=self.remove_item, accessibleName="Remove" ) self.values_edit.addActions([self.move_value_up, self.move_value_down, self.add_new_item, self.remove_item]) hlayout.addWidget(button1) hlayout.addWidget(button2) hlayout.addSpacing(3) hlayout.addWidget(button3) hlayout.addWidget(button4) hlayout.addStretch(10) vlayout.addLayout(hlayout) form.insertRow(1, "值:", vlayout) QWidget.setTabOrder(self.name_edit, self.values_edit) QWidget.setTabOrder(self.values_edit, button1) QWidget.setTabOrder(button1, button2) QWidget.setTabOrder(button2, button3) QWidget.setTabOrder(button3, button4) def set_data(self, var, transform=()): # type: (Optional[Categorical], Sequence[Transform]) -> None """ Set the variable to edit. """ super().set_data(var, transform) tr = None # type: Optional[CategoriesMapping] for tr_ in transform: if isinstance(tr_, CategoriesMapping): tr = tr_ items = [] if tr is not None: ci_index = {c: i for i, c in enumerate(var.categories)} for ci, cj in tr.mapping: if ci is None and cj is not None: # level added item = { Qt.EditRole: cj, EditStateRole: ItemEditState.Added, SourcePosRole: None } elif ci is not None and cj is None: # ci level dropped item = { Qt.EditRole: ci, EditStateRole: ItemEditState.Dropped, SourcePosRole: ci_index[ci], SourceNameRole: ci } elif ci is not None and cj is not None: # rename or reorder item = { Qt.EditRole: cj, EditStateRole: ItemEditState.NoState, SourcePosRole: ci_index[ci], SourceNameRole: ci } else: assert False, "invalid mapping: {!r}".format(tr.mapping) items.append(item) elif var is not None: items = [ {Qt.EditRole: c, EditStateRole: ItemEditState.NoState, SourcePosRole: i, SourceNameRole: c} for i, c in enumerate(var.categories) ] else: items = [] with disconnected(self.values_model.dataChanged, self.on_values_changed): self.values_model.clear() self.values_model.insertRows(0, len(items)) for i, item in enumerate(items): self.values_model.setItemData( self.values_model.index(i, 0), item ) self.add_new_item.actionGroup().setEnabled(var is not None) def __categories_mapping(self): # type: () -> CategoriesMappingType model = self.values_model source = self.var.categories res = [] for i in range(model.rowCount()): midx = model.index(i, 0) category = midx.data(Qt.EditRole) source_pos = midx.data(SourcePosRole) # type: Optional[int] if source_pos is not None: source_name = source[source_pos] else: source_name = None state = midx.data(EditStateRole) if state == ItemEditState.Dropped: res.append((source_name, None)) elif state == ItemEditState.Added: res.append((None, category)) else: res.append((source_name, category)) return res def get_data(self): """Retrieve the modified variable """ var, tr = super().get_data() if var is None: return var, tr mapping = self.__categories_mapping() if any(_1 != _2 or _2 != _3 for (_1, _2), _3 in zip_longest(mapping, var.categories)): tr.append(CategoriesMapping(mapping)) return var, tr def clear(self): """Clear the model state. """ super().clear() self.values_model.clear() def move_rows(self, rows, offset): if not rows: return assert len(rows) == 1 i = rows[0].row() if offset > 0: offset += 1 self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset) self.variable_changed.emit() def move_up(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, -1) def move_down(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, 1) @Slot() def on_values_changed(self): self.variable_changed.emit() @Slot() def on_value_selection_changed(self): rows = self.values_edit.selectionModel().selectedRows() if rows: i = rows[0].row() self.move_value_up.setEnabled(i) self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1) else: self.move_value_up.setEnabled(False) self.move_value_down.setEnabled(False) def _remove_category(self): """ Remove the current selected category. If the item is an existing category present in the source variable it is marked as removed in the view. But if it was added in the set transformation it is removed entirely from the model and view. """ view = self.values_edit rows = view.selectionModel().selectedRows(0) if not rows: return assert len(rows) == 1 index = rows[0] # type: QModelIndex model = index.model() state = index.data(EditStateRole) pos = index.data(Qt.UserRole) if pos is not None and pos >= 0: # existing level -> only mark/toggle its dropped state model.setData( index, ItemEditState.Dropped if state != ItemEditState.Dropped else ItemEditState.NoState, EditStateRole) elif state == ItemEditState.Added: # new level -> remove it model.removeRow(index.row()) else: assert False, "invalid state '{}' for {}" \ .format(state, index.row()) def _add_category(self): """ Add a new category """ view = self.values_edit model = view.model() with disconnected(model.dataChanged, self.on_values_changed, Qt.UniqueConnection): row = model.rowCount() if not model.insertRow(model.rowCount()): return index = model.index(row, 0) model.setItemData( index, { Qt.EditRole: "", SourcePosRole: None, EditStateRole: ItemEditState.Added } ) view.setCurrentIndex(index) view.edit(index) self.on_values_changed()
def showPopup(self): # type: () -> None """ Reimplemented from QComboBox.showPopup Popup up a customized view and filter edit line. Note ---- The .popup(), .lineEdit(), .completer() of the base class are not used. """ if self.__popup is not None: # We have user entered state that cannot be disturbed # (entered filter text, scroll offset, ...) return # pragma: no cover if self.count() == 0: return opt = QStyleOptionComboBox() self.initStyleOption(opt) popup = QListView( uniformItemSizes=True, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAsNeeded, iconSize=self.iconSize(), ) popup.setFocusProxy(self.__searchline) popup.setParent(self, Qt.Popup | Qt.FramelessWindowHint) popup.setItemDelegate(_ComboBoxListDelegate(popup)) proxy = QSortFilterProxyModel(popup, filterCaseSensitivity=Qt.CaseInsensitive) proxy.setFilterKeyColumn(self.modelColumn()) proxy.setSourceModel(self.model()) popup.setModel(proxy) root = proxy.mapFromSource(self.rootModelIndex()) popup.setRootIndex(root) self.__popup = popup self.__proxy = proxy self.__searchline.setText("") self.__searchline.setPlaceholderText("Filter...") self.__searchline.setVisible(True) self.__searchline.textEdited.connect(proxy.setFilterFixedString) style = self.style() # type: QStyle popuprect_origin = style.subControlRect(QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxListBoxPopup, self) # type: QRect if sys.platform == "darwin": slmargin = self.__searchline.style() \ .pixelMetric(QStyle.PM_FocusFrameVMargin) popuprect_origin.adjust(slmargin / 2, 0, -slmargin * 1.5, slmargin) popuprect_origin = QRect(self.mapToGlobal(popuprect_origin.topLeft()), popuprect_origin.size()) editrect = style.subControlRect(QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self) # type: QRect self.__searchline.setGeometry(editrect) desktop = QApplication.desktop() screenrect = desktop.availableGeometry(self) # type: QRect # get the height for the view listrect = QRect() for i in range(min(proxy.rowCount(root), self.maxVisibleItems())): index = proxy.index(i, self.modelColumn(), root) if index.isValid(): listrect = listrect.united(popup.visualRect(index)) if listrect.height() >= screenrect.height(): break window = popup.window() # type: QWidget window.ensurePolished() if window.layout() is not None: window.layout().activate() else: QApplication.sendEvent(window, QEvent(QEvent.LayoutRequest)) margins = qwidget_margin_within(popup.viewport(), window) height = (listrect.height() + 2 * popup.spacing() + margins.top() + margins.bottom()) popup_size = (QSize(popuprect_origin.width(), height).expandedTo( window.minimumSize()).boundedTo(window.maximumSize()).boundedTo( screenrect.size())) popuprect = QRect(popuprect_origin.bottomLeft(), popup_size) popuprect = dropdown_popup_geometry(popuprect, popuprect_origin, screenrect) popup.setGeometry(popuprect) current = proxy.mapFromSource(self.model().index( self.currentIndex(), self.modelColumn(), self.rootModelIndex())) popup.setCurrentIndex(current) popup.scrollTo(current, QAbstractItemView.EnsureVisible) popup.show() popup.setFocus(Qt.PopupFocusReason) popup.installEventFilter(self) popup.viewport().installEventFilter(self) popup.viewport().setMouseTracking(True) self.update() self.__popupTimer.restart()
class SortedListWidget(QWidget): sortingOrderChanged = Signal() class _MyItemDelegate(QStyledItemDelegate): def __init__(self, sortingModel, parent): QStyledItemDelegate.__init__(self, parent) self.sortingModel = sortingModel def sizeHint(self, option, index): size = QStyledItemDelegate.sizeHint(self, option, index) return QSize(size.width(), size.height() + 4) def createEditor(self, parent, option, index): cb = QComboBox(parent) cb.setModel(self.sortingModel) cb.showPopup() return cb def setEditorData(self, editor, index): pass # TODO: sensible default def setModelData(self, editor, model, index): text = editor.currentText() model.setData(index, text) def __init__(self, *args): QWidget.__init__(self, *args) self.setContentsMargins(0, 0, 0, 0) gridLayout = QGridLayout() gridLayout.setContentsMargins(0, 0, 0, 0) gridLayout.setSpacing(1) model = QStandardItemModel(self) model.rowsInserted.connect(self.__changed) model.rowsRemoved.connect(self.__changed) model.dataChanged.connect(self.__changed) self._listView = QListView(self) self._listView.setModel(model) # self._listView.setDragEnabled(True) self._listView.setDropIndicatorShown(True) self._listView.setDragDropMode(QListView.InternalMove) self._listView.viewport().setAcceptDrops(True) self._listView.setMinimumHeight(100) gridLayout.addWidget(self._listView, 0, 0, 2, 2) vButtonLayout = QVBoxLayout() self._upAction = QAction("\u2191", self, toolTip="Move up") self._upButton = QToolButton(self) self._upButton.setDefaultAction(self._upAction) self._upButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) self._downAction = QAction("\u2193", self, toolTip="Move down") self._downButton = QToolButton(self) self._downButton.setDefaultAction(self._downAction) self._downButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) vButtonLayout.addWidget(self._upButton) vButtonLayout.addWidget(self._downButton) gridLayout.addLayout(vButtonLayout, 0, 2, 2, 1) hButtonLayout = QHBoxLayout() self._addAction = QAction("+", self) self._addButton = QToolButton(self) self._addButton.setDefaultAction(self._addAction) self._removeAction = QAction("-", self) self._removeButton = QToolButton(self) self._removeButton.setDefaultAction(self._removeAction) hButtonLayout.addWidget(self._addButton) hButtonLayout.addWidget(self._removeButton) hButtonLayout.addStretch(10) gridLayout.addLayout(hButtonLayout, 2, 0, 1, 2) self.setLayout(gridLayout) self._addAction.triggered.connect(self._onAddAction) self._removeAction.triggered.connect(self._onRemoveAction) self._upAction.triggered.connect(self._onUpAction) self._downAction.triggered.connect(self._onDownAction) def sizeHint(self): size = QWidget.sizeHint(self) return QSize(size.width(), 100) def _onAddAction(self): item = QStandardItem("") item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listView.model().appendRow(item) self._listView.setCurrentIndex(item.index()) self._listView.edit(item.index()) def _onRemoveAction(self): current = self._listView.currentIndex() self._listView.model().takeRow(current.row()) def _onUpAction(self): row = self._listView.currentIndex().row() model = self._listView.model() if row > 0: items = model.takeRow(row) model.insertRow(row - 1, items) self._listView.setCurrentIndex(model.index(row - 1, 0)) def _onDownAction(self): row = self._listView.currentIndex().row() model = self._listView.model() if row < model.rowCount() and row >= 0: items = model.takeRow(row) if row == model.rowCount(): model.appendRow(items) else: model.insertRow(row + 1, items) self._listView.setCurrentIndex(model.index(row + 1, 0)) def setModel(self, model): """ Set a model to select items from """ self._model = model self._listView.setItemDelegate(self._MyItemDelegate(self._model, self)) def addItem(self, *args): """ Add a new entry in the list """ item = QStandardItem(*args) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listView.model().appendRow(item) def setItems(self, items): self._listView.model().clear() for item in items: self.addItem(item) def items(self): order = [] for row in range(self._listView.model().rowCount()): order.append(str(self._listView.model().item(row, 0).text())) return order def __changed(self): self.sortingOrderChanged.emit() sortingOrder = property(items, setItems)
class DiscreteVariableEditor(VariableEditor): """An editor widget for editing a discrete variable. Extends the :class:`VariableEditor` to enable editing of variables values. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) form = self.layout().itemAt(0) assert isinstance(form, QFormLayout) #: A list model of discrete variable's values. self.values_model = itemmodels.PyListModel( flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable ) vlayout = QVBoxLayout(spacing=1, margin=0) self.values_edit = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed ) self.values_edit.setItemDelegate(CategoriesEditDelegate(self)) self.values_edit.setModel(self.values_model) self.values_model.dataChanged.connect(self.on_values_changed) self.values_edit.selectionModel().selectionChanged.connect( self.on_value_selection_changed) self.values_model.layoutChanged.connect(self.on_value_selection_changed) self.values_model.rowsMoved.connect(self.on_value_selection_changed) vlayout.addWidget(self.values_edit) hlayout = QHBoxLayout(spacing=1, margin=0) self.categories_action_group = group = QActionGroup( self, objectName="action-group-categories", enabled=False ) self.move_value_up = QAction( "\N{UPWARDS ARROW}", group, toolTip="Move the selected item up.", shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_BracketLeft), shortcutContext=Qt.WidgetShortcut, ) self.move_value_up.triggered.connect(self.move_up) self.move_value_down = QAction( "\N{DOWNWARDS ARROW}", group, toolTip="Move the selected item down.", shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_BracketRight), shortcutContext=Qt.WidgetShortcut, ) self.move_value_down.triggered.connect(self.move_down) self.add_new_item = QAction( "+", group, objectName="action-add-item", toolTip="Append a new item.", shortcut=QKeySequence(QKeySequence.New), shortcutContext=Qt.WidgetShortcut, ) self.remove_item = QAction( "\N{MINUS SIGN}", group, objectName="action-remove-item", toolTip="Delete the selected item.", shortcut=QKeySequence(QKeySequence.Delete), shortcutContext=Qt.WidgetShortcut, ) self.add_new_item.triggered.connect(self._add_category) self.remove_item.triggered.connect(self._remove_category) button1 = FixedSizeButton( self, defaultAction=self.move_value_up, accessibleName="Move up" ) button2 = FixedSizeButton( self, defaultAction=self.move_value_down, accessibleName="Move down" ) button3 = FixedSizeButton( self, defaultAction=self.add_new_item, accessibleName="Add" ) button4 = FixedSizeButton( self, defaultAction=self.remove_item, accessibleName="Remove" ) self.values_edit.addActions([self.move_value_up, self.move_value_down, self.add_new_item, self.remove_item]) hlayout.addWidget(button1) hlayout.addWidget(button2) hlayout.addSpacing(3) hlayout.addWidget(button3) hlayout.addWidget(button4) hlayout.addStretch(10) vlayout.addLayout(hlayout) form.insertRow(1, "Values:", vlayout) QWidget.setTabOrder(self.name_edit, self.values_edit) QWidget.setTabOrder(self.values_edit, button1) QWidget.setTabOrder(button1, button2) QWidget.setTabOrder(button2, button3) QWidget.setTabOrder(button3, button4) def set_data(self, var, transform=()): # type: (Optional[Categorical], Sequence[Transform]) -> None """ Set the variable to edit. """ super().set_data(var, transform) tr = None # type: Optional[CategoriesMapping] for tr_ in transform: if isinstance(tr_, CategoriesMapping): tr = tr_ items = [] if tr is not None: ci_index = {c: i for i, c in enumerate(var.categories)} for ci, cj in tr.mapping: if ci is None and cj is not None: # level added item = { Qt.EditRole: cj, EditStateRole: ItemEditState.Added, SourcePosRole: None } elif ci is not None and cj is None: # ci level dropped item = { Qt.EditRole: ci, EditStateRole: ItemEditState.Dropped, SourcePosRole: ci_index[ci], SourceNameRole: ci } elif ci is not None and cj is not None: # rename or reorder item = { Qt.EditRole: cj, EditStateRole: ItemEditState.NoState, SourcePosRole: ci_index[ci], SourceNameRole: ci } else: assert False, "invalid mapping: {!r}".format(tr.mapping) items.append(item) elif var is not None: items = [ {Qt.EditRole: c, EditStateRole: ItemEditState.NoState, SourcePosRole: i, SourceNameRole: c} for i, c in enumerate(var.categories) ] else: items = [] with disconnected(self.values_model.dataChanged, self.on_values_changed): self.values_model.clear() self.values_model.insertRows(0, len(items)) for i, item in enumerate(items): self.values_model.setItemData( self.values_model.index(i, 0), item ) self.add_new_item.actionGroup().setEnabled(var is not None) def __categories_mapping(self): # type: () -> CategoriesMappingType model = self.values_model source = self.var.categories res = [] for i in range(model.rowCount()): midx = model.index(i, 0) category = midx.data(Qt.EditRole) source_pos = midx.data(SourcePosRole) # type: Optional[int] if source_pos is not None: source_name = source[source_pos] else: source_name = None state = midx.data(EditStateRole) if state == ItemEditState.Dropped: res.append((source_name, None)) elif state == ItemEditState.Added: res.append((None, category)) else: res.append((source_name, category)) return res def get_data(self): """Retrieve the modified variable """ var, tr = super().get_data() if var is None: return var, tr mapping = self.__categories_mapping() if any(_1 != _2 or _2 != _3 for (_1, _2), _3 in zip_longest(mapping, var.categories)): tr.append(CategoriesMapping(mapping)) return var, tr def clear(self): """Clear the model state. """ super().clear() self.values_model.clear() def move_rows(self, rows, offset): if not rows: return assert len(rows) == 1 i = rows[0].row() if offset > 0: offset += 1 self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset) self.variable_changed.emit() def move_up(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, -1) def move_down(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, 1) @Slot() def on_values_changed(self): self.variable_changed.emit() @Slot() def on_value_selection_changed(self): rows = self.values_edit.selectionModel().selectedRows() if rows: i = rows[0].row() self.move_value_up.setEnabled(i) self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1) else: self.move_value_up.setEnabled(False) self.move_value_down.setEnabled(False) def _remove_category(self): """ Remove the current selected category. If the item is an existing category present in the source variable it is marked as removed in the view. But if it was added in the set transformation it is removed entirely from the model and view. """ view = self.values_edit rows = view.selectionModel().selectedRows(0) if not rows: return assert len(rows) == 1 index = rows[0] # type: QModelIndex model = index.model() state = index.data(EditStateRole) pos = index.data(Qt.UserRole) if pos is not None and pos >= 0: # existing level -> only mark/toggle its dropped state model.setData( index, ItemEditState.Dropped if state != ItemEditState.Dropped else ItemEditState.NoState, EditStateRole) elif state == ItemEditState.Added: # new level -> remove it model.removeRow(index.row()) else: assert False, "invalid state '{}' for {}" \ .format(state, index.row()) def _add_category(self): """ Add a new category """ view = self.values_edit model = view.model() with disconnected(model.dataChanged, self.on_values_changed, Qt.UniqueConnection): row = model.rowCount() if not model.insertRow(model.rowCount()): return index = model.index(row, 0) model.setItemData( index, { Qt.EditRole: "", SourcePosRole: None, EditStateRole: ItemEditState.Added } ) view.setCurrentIndex(index) view.edit(index) self.on_values_changed()
class OWPythonScript(widget.OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." icon = "icons/PythonScript.svg" priority = 3150 class Inputs: data = Input("Data", Table, replaces=["in_data"], default=True, multiple=True) learner = Input("Learner", Learner, replaces=["in_learner"], default=True, multiple=True) classifier = Input("Classifier", Model, replaces=["in_classifier"], default=True, multiple=True) object = Input("Object", object, replaces=["in_object"], default=False, multiple=True) class Outputs: data = Output("Data", Table, replaces=["out_data"]) learner = Output("Learner", Learner, replaces=["out_learner"]) classifier = Output("Classifier", Model, replaces=["out_classifier"]) object = Output("Object", object, replaces=["out_object"]) signal_names = ("data", "learner", "classifier", "object") libraryListSource = Setting( [Script("Hello world", "print('Hello world')\n")]) currentScriptIndex = Setting(0) splitterState = Setting(None) auto_execute = Setting(True) class Error(OWWidget.Error): pass def __init__(self): super().__init__() for name in self.signal_names: setattr(self, name, {}) for s in self.libraryListSource: s.flags = 0 self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, "Info") gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + "<li>".join(map("in_{0}, in_{0}s".format, self.signal_names)) + "</ul></p><p>Output variables:<ul><li>" + "<li>".join(map("out_{0}".format, self.signal_names)) + "</ul></p>", ) self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, "Library") self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred), ) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) auto = gui.auto_commit( self.controlArea, self, "auto_execute", "Run", checkbox_label="Autorun on new data", ) self.execute_button, self.autobox = auto.button, auto.checkbox self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = ("Monaco" if sys.platform == "darwin" else "Courier") self.textBox = gui.vBox(self, "Python Script") self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, "Console") self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) select_row(self.libraryView, self.currentScriptIndex) self.splitCanvas.setSizes([2, 1]) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) def enable_execute(self): self.execute_button.setEnabled(True) def handle_input(self, obj, id, signal): id = id[0] dic = getattr(self, signal) if obj is None: if id in dic.keys(): del dic[id] else: dic[id] = obj @Inputs.data def set_data(self, data, id): self.handle_input(data, id, "data") @Inputs.learner def set_learner(self, data, id): self.handle_input(data, id, "learner") @Inputs.classifier def set_classifier(self, data, id): self.handle_input(data, id, "classifier") @Inputs.object def set_object(self, data, id): self.handle_input(data, id, "object") def handleNewSignals(self): self.unconditional_commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *args): self.libraryList.append(Script("New script", "", 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *args): filename, _ = QFileDialog.getOpenFileName( self, "Open Python Script", os.path.expanduser("~/"), "Python files (*.py)\nAll files(*.*)", ) if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *args): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *args): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if type(script) != Script: script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *args): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def onSpliterMoved(self, pos, ind): self.splitterState = bytes(self.splitCanvas.saveState()) def updateSelecetdScriptState(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] self.libraryList[index] = Script(script.name, self.text.toPlainText(), 0) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, "Save Python Script", filename, "Python files (*.py)\nAll files(*.*)") if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, "w") f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = {} for name in self.signal_names: value = getattr(self, name) all_values = list(value.values()) one_value = all_values[0] if len(all_values) == 1 else None d["in_" + name + "s"] = all_values d["in_" + name] = one_value return d def commit(self): if self.auto_execute: self.execute_button.setEnabled(False) self.Error.clear() self._script = str(self.text.toPlainText()) lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for signal in self.signal_names: out_var = self.console.locals.get("out_" + signal) signal_type = getattr(self.Outputs, signal).type if not isinstance(out_var, signal_type) and out_var is not None: self.Error.add_message( signal, "'{}' has to be an instance of '{}'.".format( signal, signal_type.__name__), ) getattr(self.Error, signal)() out_var = None getattr(self.Outputs, signal).send(out_var)
class OWPythonScript(widget.OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." icon = "icons/PythonScript.svg" priority = 3150 inputs = [("in_data", Orange.data.Table, "setExampleTable", widget.Default), # ("in_distance", Orange.misc.SymMatrix, "setDistanceMatrix", # widget.Default), ("in_learner", Learner, "setLearner", widget.Default), ("in_classifier", Model, "setClassifier", widget.Default), ("in_object", object, "setObject")] outputs = [("out_data", Orange.data.Table, ), # ("out_distance", Orange.misc.SymMatrix, ), ("out_learner", Learner, ), ("out_classifier", Model, widget.Dynamic), ("out_object", object, widget.Dynamic)] libraryListSource = \ Setting([Script("Hello world", "print('Hello world')\n")]) currentScriptIndex = Setting(0) splitterState = Setting(None) auto_execute = Setting(False) def __init__(self): super().__init__() self.in_data = None self.in_distance = None self.in_learner = None self.in_classifier = None self.in_object = None self.auto_execute = False for s in self.libraryListSource: s.flags = 0 self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + \ "<li>".join(t.name for t in self.inputs) + \ "</ul></p><p>Output variables:<ul><li>" + \ "<li>".join(t.name for t in self.outputs) + \ "</ul></p>" ) self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) ) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged ) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.auto_commit( self.controlArea, self, "auto_execute", "Execute", auto_label="Auto Execute") self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.vBox(self, 'Python Script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) select_row(self.libraryView, self.currentScriptIndex) self.splitCanvas.setSizes([2, 1]) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) def setExampleTable(self, et): self.in_data = et def setDistanceMatrix(self, dm): self.in_distance = dm def setLearner(self, learner): self.in_learner = learner def setClassifier(self, classifier): self.in_classifier = classifier def setObject(self, obj): self.in_object = obj def handleNewSignals(self): self.unconditional_commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *args): self.libraryList.append(Script("New script", "", 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *args): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)' ) if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *args): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *args): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if type(script) != Script: script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *args): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def onSpliterMoved(self, pos, ind): self.splitterState = bytes(self.splitCanvas.saveState()) def updateSelecetdScriptState(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] self.libraryList[index] = Script(script.name, self.text.toPlainText(), 0) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)' ) if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = dict([(i.name, getattr(self, i.name, None)) for i in self.inputs]) d.update(dict([(o.name, None) for o in self.outputs])) return d def commit(self): self._script = str(self.text.toPlainText()) lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for out in self.outputs: signal = out.name self.send(signal, self.console.locals.get(signal, None))
class OWDiscretize(widget.OWWidget): name = "离散化(Discretize)" description = "离散化数值数据特征" icon = "icons/Discretize.svg" keywords = [] class Inputs: data = Input("数据(Data)", Orange.data.Table, doc="Input data table", replaces=['Data']) class Outputs: data = Output("数据(Data)", Orange.data.Table, doc="Table with discretized features", replaces=['Data']) settingsHandler = settings.DomainContextHandler() saved_var_states = settings.ContextSetting({}) default_method = settings.Setting(2) default_k = settings.Setting(3) autosend = settings.Setting(True) #: Discretization methods Default, Leave, MDL, EqualFreq, EqualWidth, Remove, Custom = range(7) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() #: input data self.data = None self.class_var = None #: Current variable discretization state self.var_state = {} #: Saved variable discretization settings (context setting) self.saved_var_states = {} self.method = 0 self.k = 5 self.cutpoints = [] box = gui.vBox(self.controlArea, self.tr("默认离散化")) self.default_bbox = rbox = gui.radioButtons( box, self, "default_method", callback=self._default_disc_changed) rb = gui.hBox(rbox) self.left = gui.vBox(rb) right = gui.vBox(rb) rb.layout().setStretch(0, 1) rb.layout().setStretch(1, 1) options = self.options = [ self.tr("默认"), self.tr("保留原值"), self.tr("熵MDL离散化(Entropy-MDL discretization)"), self.tr("等频离散化(Equal-frequency discretization)"), self.tr("等宽离散化(Equal-width discretization)"), self.tr("删除数值变量") ] for opt in options[1:]: t = gui.appendRadioButton(rbox, opt) # This condition is ugly, but it keeps the same order of # options for backward compatibility of saved schemata [right, self.left][opt.startswith("Equal")].layout().addWidget(t) gui.separator(right, 18, 18) def _intbox(parent, attr, callback): box = gui.indentedBox(parent) s = gui.spin( box, self, attr, minv=2, maxv=10, label="间隔数(Num. of intervals):", callback=callback) s.setMaximumWidth(60) s.setAlignment(Qt.AlignRight) gui.rubber(s.box) return box.box self.k_general = _intbox(self.left, "default_k", self._default_disc_changed) self.k_general.layout().setContentsMargins(0, 0, 0, 0) vlayout = QHBoxLayout() box = gui.widgetBox( self.controlArea, "单个属性设置", orientation=vlayout, spacing=8 ) # List view with all attributes self.varview = QListView( selectionMode=QListView.ExtendedSelection, uniformItemSizes=True, ) self.varview.setItemDelegate(DiscDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._var_selection_changed ) vlayout.addWidget(self.varview) # Controls for individual attr settings self.bbox = controlbox = gui.radioButtons( box, self, "method", callback=self._disc_method_changed ) vlayout.addWidget(controlbox) for opt in options[:5]: gui.appendRadioButton(controlbox, opt) self.k_specific = _intbox(controlbox, "k", self._disc_method_changed) gui.appendRadioButton(controlbox, "删除属性") gui.rubber(controlbox) controlbox.setEnabled(False) self.controlbox = controlbox box = gui.auto_apply(self.controlArea, self, "autosend") box.button.setFixedWidth(180) box.layout().insertStretch(0) self._update_spin_positions() self.info.set_input_summary(self.info.NoInput) self.info.set_output_summary(self.info.NoOutput) @Inputs.data def set_data(self, data): self.closeContext() self.data = data if self.data is not None: self._initialize(data) self.openContext(data) # Restore the per variable discretization settings self._restore(self.saved_var_states) # Complete the induction of cut points self._update_points() self.info.set_input_summary(len(data)) else: self.info.set_input_summary(self.info.NoInput) self._clear() self.unconditional_commit() def _initialize(self, data): # Initialize the default variable states for new data. self.class_var = data.domain.class_var cvars = [var for var in data.domain.variables if var.is_continuous] self.varmodel[:] = cvars has_disc_class = data.domain.has_discrete_class self.default_bbox.buttons[self.MDL - 1].setEnabled(has_disc_class) self.bbox.buttons[self.MDL].setEnabled(has_disc_class) # If the newly disabled MDL button is checked then change it if not has_disc_class and self.default_method == self.MDL - 1: self.default_method = 0 if not has_disc_class and self.method == self.MDL: self.method = 0 # Reset (initialize) the variable discretization states. self._reset() def _restore(self, saved_state): # Restore variable states from a saved_state dictionary. def_method = self._current_default_method() for i, var in enumerate(self.varmodel): key = variable_key(var) if key in saved_state: state = saved_state[key] if isinstance(state.method, Default): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _reset(self): # restore the individual variable settings back to defaults. def_method = self._current_default_method() self.var_state = {} for i in range(len(self.varmodel)): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _set_var_state(self, index, state): # set the state of variable at `index` to `state`. self.var_state[index] = state self.varmodel.setData(self.varmodel.index(index), state, Qt.UserRole) def _clear(self): self.data = None self.varmodel[:] = [] self.var_state = {} self.saved_var_states = {} self.default_bbox.buttons[self.MDL - 1].setEnabled(True) self.bbox.buttons[self.MDL].setEnabled(True) def _update_points(self): """ Update the induced cut points. """ if self.data is None or not len(self.data): return def induce_cuts(method, data, var): dvar = _dispatch[type(method)](method, data, var) if dvar is None: # removed return [], None elif dvar is var: # no transformation took place return None, var elif is_discretized(dvar): return dvar.compute_value.points, dvar assert False return None for i, var in enumerate(self.varmodel): state = self.var_state[i] if state.points is None and state.disc_var is None: points, dvar = induce_cuts(state.method, self.data, var) new_state = state._replace(points=points, disc_var=dvar) self._set_var_state(i, new_state) @staticmethod def _method_index(method): return METHODS.index((type(method), )) def _current_default_method(self): method = self.default_method + 1 k = self.default_k if method == OWDiscretize.Leave: def_method = Leave() elif method == OWDiscretize.MDL: def_method = MDL() elif method == OWDiscretize.EqualFreq: def_method = EqualFreq(k) elif method == OWDiscretize.EqualWidth: def_method = EqualWidth(k) elif method == OWDiscretize.Remove: def_method = Remove() else: assert False return def_method def _current_method(self): if self.method == OWDiscretize.Default: method = Default(self._current_default_method()) elif self.method == OWDiscretize.Leave: method = Leave() elif self.method == OWDiscretize.MDL: method = MDL() elif self.method == OWDiscretize.EqualFreq: method = EqualFreq(self.k) elif self.method == OWDiscretize.EqualWidth: method = EqualWidth(self.k) elif self.method == OWDiscretize.Remove: method = Remove() elif self.method == OWDiscretize.Custom: method = Custom(self.cutpoints) else: assert False return method def _update_spin_positions(self): self.k_general.setDisabled(self.default_method not in [2, 3]) if self.default_method == 2: self.left.layout().insertWidget(1, self.k_general) elif self.default_method == 3: self.left.layout().insertWidget(2, self.k_general) self.k_specific.setDisabled(self.method not in [3, 4]) if self.method == 3: self.bbox.layout().insertWidget(4, self.k_specific) elif self.method == 4: self.bbox.layout().insertWidget(5, self.k_specific) def _default_disc_changed(self): self._update_spin_positions() method = self._current_default_method() state = DState(Default(method), None, None) for i, _ in enumerate(self.varmodel): if isinstance(self.var_state[i].method, Default): self._set_var_state(i, state) self._update_points() self.commit() def _disc_method_changed(self): self._update_spin_positions() indices = self.selected_indices() method = self._current_method() state = DState(method, None, None) for idx in indices: self._set_var_state(idx, state) self._update_points() self.commit() def _var_selection_changed(self, *_): indices = self.selected_indices() # set of all methods for the current selection methods = [self.var_state[i].method for i in indices] mset = set(methods) self.controlbox.setEnabled(len(mset) > 0) if len(mset) == 1: method = mset.pop() self.method = self._method_index(method) if isinstance(method, (EqualFreq, EqualWidth)): self.k = method.k elif isinstance(method, Custom): self.cutpoints = method.points else: # deselect the current button self.method = -1 bg = self.controlbox.group button_group_reset(bg) self._update_spin_positions() def selected_indices(self): rows = self.varview.selectionModel().selectedRows() return [index.row() for index in rows] def discretized_var(self, index): # type: (int) -> Optional[Orange.data.DiscreteVariable] state = self.var_state[index] if state.disc_var is not None and state.points == []: # Removed by MDL Entropy return None else: return state.disc_var def discretized_domain(self): """ Return the current effective discretized domain. """ if self.data is None: return None # a mapping of all applied changes for variables in `varmodel` mapping = {var: self.discretized_var(i) for i, var in enumerate(self.varmodel)} def disc_var(source): return mapping.get(source, source) # map the full input domain to the new variables (where applicable) attributes = [disc_var(v) for v in self.data.domain.attributes] attributes = [v for v in attributes if v is not None] class_vars = [disc_var(v) for v in self.data.domain.class_vars] class_vars = [v for v in class_vars if v is not None] domain = Orange.data.Domain( attributes, class_vars, metas=self.data.domain.metas ) return domain def commit(self): output = None if self.data is not None and len(self.data): domain = self.discretized_domain() output = self.data.transform(domain) self.info.set_output_summary(len(output)) else: self.info.set_output_summary(self.info.NoOutput) self.Outputs.data.send(output) def storeSpecificSettings(self): super().storeSpecificSettings() self.saved_var_states = { variable_key(var): self.var_state[i]._replace(points=None, disc_var=None) for i, var in enumerate(self.varmodel) } def send_report(self): self.report_items(( ("Default method", self.options[self.default_method + 1]),)) if self.varmodel: self.report_items("Thresholds", [ (var.name, DiscDelegate.cutsText(self.var_state[i]) or "leave numeric") for i, var in enumerate(self.varmodel)])
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 class Inputs: data = Input("Data", Orange.data.Table) learner = Input("Learner", Learner) class Outputs: data = Output("Data", Orange.data.Table) class Error(OWWidget.Error): imputation_failed = Msg("Imputation failed for '{}'") model_based_imputer_sparse = Msg("Model based imputer does not work for sparse data") DEFAULT_LEARNER = SimpleTreeLearner() METHODS = [AsDefault(), impute.DoNotImpute(), impute.Average(), impute.AsValue(), impute.Model(DEFAULT_LEARNER), impute.Random(), impute.DropInstances(), impute.Default()] DEFAULT, DO_NOT_IMPUTE, MODEL_BASED_IMPUTER, AS_INPUT = 0, 1, 4, 7 settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(DO_NOT_IMPUTE) variable_methods = settings.ContextSetting({}) autocommit = settings.Setting(True) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() # copy METHODS (some are modified by the widget) self.methods = copy.deepcopy(OWImpute.METHODS) main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for i, method in enumerate(self.methods): if not method.columns_only: button = QRadioButton(method.name) button.setChecked(i == self.default_method_index) button_group.addButton(button, i) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView( selectionMode=QListView.ExtendedSelection ) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed ) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for i, method in enumerate(self.methods): button = QRadioButton(text=method.name) button_group.addButton(button, i) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected ) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, ) self.value_stack = value_stack = QStackedWidget() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addWidget(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection ) method_layout.addStretch(2) reset_button = QPushButton( "Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_methods, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit( self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 80) box.layout().insertWidget(0, self.report_button) self.data = None self.learner = None self.modified = False self.default_method = self.methods[self.default_method_index] self.executor = qconcurrent.ThreadExecutor(self) self.__task = None @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: self._default_method_index = index self.default_button_group.button(index).setChecked(True) self.default_method = self.methods[self.default_method_index] self.methods[self.DEFAULT].method = self.default_method # update variable view for index in map(self.varmodel.index, range(len(self.varmodel))): method = self.variable_methods.get( index.row(), self.methods[self.DEFAULT]) self.varmodel.setData(index, method, Qt.UserRole) self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @Inputs.data @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self.variable_methods = {} self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) self.update_varview() self.unconditional_commit() @Inputs.learner def set_learner(self, learner): self.learner = learner or self.DEFAULT_LEARNER imputer = self.methods[self.MODEL_BASED_IMPUTER] imputer.learner = self.learner button = self.default_button_group.button(self.MODEL_BASED_IMPUTER) button.setText(imputer.name) variable_button = self.variable_button_group.button(self.MODEL_BASED_IMPUTER) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = self.MODEL_BASED_IMPUTER self.update_varview() self.commit() def get_method_for_column(self, column_index): """Returns the imputation method for column by its index. """ if not isinstance(column_index, int): column_index = column_index.row() return self.variable_methods.get(column_index, self.methods[self.DEFAULT]) def _invalidate(self): self.modified = True if self.__task is not None: self.cancel() self.commit() def commit(self): self.cancel() self.warning() self.Error.imputation_failed.clear() self.Error.model_based_imputer_sparse.clear() if self.data is None or len(self.data) == 0 or len(self.varmodel) == 0: self.Outputs.data.send(self.data) self.modified = False return data = self.data impute_state = [ (i, var, self.variable_methods.get(i, self.default_method)) for i, var in enumerate(self.varmodel) ] def impute_one(method, var, data): # type: (impute.BaseImputeMethod, Variable, Table) -> Any if isinstance(method, impute.Model) and data.is_sparse(): raise SparseNotSupported() elif isinstance(method, impute.DropInstances): return RowMask(method(data, var)) elif not method.supports_variable(var): raise VariableNotSupported(var) else: return method(data, var) futures = [] for _, var, method in impute_state: f = self.executor.submit( impute_one, copy.deepcopy(method), var, data) futures.append(f) w = qconcurrent.FutureSetWatcher(futures) w.doneAll.connect(self.__commit_finish) w.progressChanged.connect(self.__progress_changed) self.__task = Task(futures, w) self.progressBarInit(processEvents=False) self.setBlocking(True) @Slot() def __commit_finish(self): assert QThread.currentThread() is self.thread() assert self.__task is not None futures = self.__task.futures assert len(futures) == len(self.varmodel) assert self.data is not None self.__task = None self.setBlocking(False) self.progressBarFinished() data = self.data attributes = [] class_vars = [] drop_mask = np.zeros(len(self.data), bool) for i, (var, fut) in enumerate(zip(self.varmodel, futures)): assert fut.done() newvar = [] try: res = fut.result() except SparseNotSupported: self.Error.model_based_imputer_sparse() # ?? break except VariableNotSupported: self.warning("Default method can not handle '{}'". format(var.name)) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error for %s", var, exc_info=True) self.Error.imputation_failed(var.name) attributes = class_vars = None break else: if isinstance(res, RowMask): drop_mask |= res.mask newvar = var else: newvar = res if isinstance(newvar, Orange.data.Variable): newvar = [newvar] if i < len(data.domain.attributes): attributes.extend(newvar) else: class_vars.extend(newvar) if attributes is None: data = None else: domain = Orange.data.Domain(attributes, class_vars, data.domain.metas) try: data = self.data.from_table(domain, data[~drop_mask]) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error", exc_info=True) self.Error.imputation_failed("Unknown") data = None self.Outputs.data.send(data) self.modified = False @Slot(int, int) def __progress_changed(self, n, d): assert QThread.currentThread() is self.thread() assert self.__task is not None self.progressBarSet(100. * n / d) def cancel(self): if self.__task is not None: task, self.__task = self.__task, None task.cancel() task.watcher.doneAll.disconnect(self.__commit_finish) task.watcher.progressChanged.disconnect(self.__progress_changed) concurrent.futures.wait(task.futures) task.watcher.flush() self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, None) if method is not None: specific.append("{} ({})".format(var.name, str(method))) default = self.default_method.name if specific: self.report_items(( ("Default method", default), ("Specific imputers", ", ".join(specific)) )) else: self.report_items((("Method", default),)) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() methods = [self.get_method_for_column(i.row()) for i in indexes] def method_key(method): """ Decompose method into its type and parameters. """ # The return value should be hashable and __eq__ comparable if isinstance(method, AsDefault): return AsDefault, (method.method,) elif isinstance(method, impute.Model): return impute.Model, (method.learner,) elif isinstance(method, impute.Default): return impute.Default, (method.default,) else: return type(method), None methods = set(method_key(m) for m in methods) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) fixed_value = None value_stack_enabled = False current_value_widget = None if len(methods) == 1: method_type, parameters = methods.pop() for i, m in enumerate(self.methods): if method_type == type(m): self.variable_button_group.button(i).setChecked(True) if method_type is impute.Default: (fixed_value,) = parameters elif self.variable_button_group.checkedButton() is not None: # Uncheck the current button self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) assert self.variable_button_group.checkedButton() is None for method, button in zip(self.methods, self.variable_button_group.buttons()): enabled = all(method.supports_variable(var) for var in selected_vars) button.setEnabled(enabled) if not has_discrete: value_stack_enabled = True current_value_widget = self.value_double elif len(selected_vars) == 1: value_stack_enabled = True current_value_widget = self.value_combo self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) else: value_stack_enabled = False current_value_widget = None self.variable_button_group.button(self.AS_INPUT).setEnabled(False) self.value_stack.setEnabled(value_stack_enabled) if current_value_widget is not None: self.value_stack.setCurrentWidget(current_value_widget) if fixed_value is not None: if current_value_widget is self.value_combo: self.value_combo.setCurrentIndex(fixed_value) elif current_value_widget is self.value_double: self.value_double.setValue(fixed_value) else: assert False def set_method_for_current_selection(self, method_index): indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): if method_index == self.DEFAULT: for index in indexes: self.variable_methods.pop(index.row(), None) elif method_index == OWImpute.AS_INPUT: current = self.value_stack.currentWidget() if current is self.value_combo: value = self.value_combo.currentIndex() else: value = self.value_double.value() for index in indexes: method = impute.Default(default=value) self.variable_methods[index.row()] = method else: method = self.methods[method_index] for index in indexes: self.variable_methods[index.row()] = method self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), Qt.UserRole) def _on_value_selected(self): # The fixed 'Value' in the widget has been changed by the user. self.variable_button_group.button(self.AS_INPUT).setChecked(True) self.set_method_for_current_selection(self.AS_INPUT) def reset_variable_methods(self): indexes = list(map(self.varmodel.index, range(len(self.varmodel)))) self.set_method_for_indexes(indexes, self.DEFAULT) self.variable_button_group.button(self.DEFAULT).setChecked(True)
class OWFeatureConstructor(OWWidget): name = "Feature Constructor" description = "Construct new features (data columns) from a set of " \ "existing features in the input dataset." icon = "icons/FeatureConstructor.svg" keywords = [] class Inputs: data = Input("Data", Orange.data.Table) class Outputs: data = Output("Data", Orange.data.Table) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) EDITORS = [(ContinuousDescriptor, ContinuousFeatureEditor), (DateTimeDescriptor, DateTimeFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor)] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "Variable Definitions") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget(sizePolicy=QSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton("New", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def reserved_names(): varnames = [] if self.data is not None: varnames = [ var.name for var in self.data.domain.variables + self.data.domain.metas ] varnames += [desc.name for desc in self.featuremodel] return set(varnames) def generate_newname(fmt): return unique_name(fmt, reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("Numeric") cont.triggered.connect(lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3))) disc = menu.addAction("Categorical") disc.triggered.connect(lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", (), False))) string = menu.addAction("Text") string.triggered.connect(lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), ""))) datetime = menu.addAction("Date/Time") datetime.triggered.connect(lambda: self.addFeature( DateTimeDescriptor(generate_newname("T{}"), ""))) menu.addSeparator() self.duplicateaction = menu.addAction("Duplicate Selected Variable") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton("Remove", toolTip="Remove selected variable", minimumWidth=120, shortcut=QKeySequence.Delete) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView(minimumWidth=200, sizePolicy=QSizePolicy( QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) box = gui.hBox(self.controlArea) gui.rubber(box) commit = gui.button(box, self, "Send", callback=self.apply, default=True) commit.setMinimumWidth(180) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: editor = self.editorstack.currentWidget() self.featuremodel[self.currentIndex] = editor.editorData() self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data if self.data is not None: descriptors = list(self.descriptors) currindex = self.currentIndex self.descriptors = [] self.currentIndex = -1 self.openContext(data) if descriptors != self.descriptors or \ self.currentIndex != currindex: # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect( self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect( self._on_selectedVariableChanged) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.Outputs.data.send(None) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and self.featuremodel.rowCount(): # Deleting the last item clears selection self.setCurrentIndex(self.featuremodel.rowCount() - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) @staticmethod def check_attrs_values(attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) # ast.parse can return arbitrary errors, not only SyntaxError # pylint: disable=broad-except except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): def report_error(err): log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) try: new_variables = construct_variables(desc, self.data) # user's expression can contain arbitrary errors except Exception as err: # pylint: disable=broad-except report_error(err) return attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas)) try: data = self.data.transform(new_domain) # user's expression can contain arbitrary errors # pylint: disable=broad-except except Exception as err: report_error(err) return disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): items[ feature.name] = "{} (categorical with values {}{})".format( feature.expression, feature.values, "; ordered" * feature.ordered) elif isinstance(feature, ContinuousDescriptor): items[feature.name] = "{} (numeric)".format(feature.expression) elif isinstance(feature, DateTimeDescriptor): items[feature.name] = "{} (date/time)".format( feature.expression) else: items[feature.name] = "{} (text)".format(feature.expression) self.report_items(report.plural("Constructed feature{s}", len(items)), items)
class SortedListWidget(QWidget): sortingOrderChanged = Signal() class _MyItemDelegate(QStyledItemDelegate): def __init__(self, sortingModel, parent): QStyledItemDelegate.__init__(self, parent) self.sortingModel = sortingModel def sizeHint(self, option, index): size = QStyledItemDelegate.sizeHint(self, option, index) return QSize(size.width(), size.height() + 4) def createEditor(self, parent, option, index): cb = QComboBox(parent) cb.setModel(self.sortingModel) cb.showPopup() return cb def setEditorData(self, editor, index): pass # TODO: sensible default def setModelData(self, editor, model, index): text = editor.currentText() model.setData(index, text) def __init__(self, *args): QWidget.__init__(self, *args) self.setContentsMargins(0, 0, 0, 0) gridLayout = QGridLayout() gridLayout.setContentsMargins(0, 0, 0, 0) gridLayout.setSpacing(1) model = QStandardItemModel(self) model.rowsInserted.connect(self.__changed) model.rowsRemoved.connect(self.__changed) model.dataChanged.connect(self.__changed) self._listView = QListView(self) self._listView.setModel(model) # self._listView.setDragEnabled(True) self._listView.setDropIndicatorShown(True) self._listView.setDragDropMode(QListView.InternalMove) self._listView.viewport().setAcceptDrops(True) self._listView.setMinimumHeight(100) gridLayout.addWidget(self._listView, 0, 0, 2, 2) vButtonLayout = QVBoxLayout() self._upAction = QAction( "\u2191", self, toolTip="Move up") self._upButton = QToolButton(self) self._upButton.setDefaultAction(self._upAction) self._upButton.setSizePolicy( QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) self._downAction = QAction( "\u2193", self, toolTip="Move down") self._downButton = QToolButton(self) self._downButton.setDefaultAction(self._downAction) self._downButton.setSizePolicy( QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) vButtonLayout.addWidget(self._upButton) vButtonLayout.addWidget(self._downButton) gridLayout.addLayout(vButtonLayout, 0, 2, 2, 1) hButtonLayout = QHBoxLayout() self._addAction = QAction("+", self) self._addButton = QToolButton(self) self._addButton.setDefaultAction(self._addAction) self._removeAction = QAction("-", self) self._removeButton = QToolButton(self) self._removeButton.setDefaultAction(self._removeAction) hButtonLayout.addWidget(self._addButton) hButtonLayout.addWidget(self._removeButton) hButtonLayout.addStretch(10) gridLayout.addLayout(hButtonLayout, 2, 0, 1, 2) self.setLayout(gridLayout) self._addAction.triggered.connect(self._onAddAction) self._removeAction.triggered.connect(self._onRemoveAction) self._upAction.triggered.connect(self._onUpAction) self._downAction.triggered.connect(self._onDownAction) def sizeHint(self): size = QWidget.sizeHint(self) return QSize(size.width(), 100) def _onAddAction(self): item = QStandardItem("") item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listView.model().appendRow(item) self._listView.setCurrentIndex(item.index()) self._listView.edit(item.index()) def _onRemoveAction(self): current = self._listView.currentIndex() self._listView.model().takeRow(current.row()) def _onUpAction(self): row = self._listView.currentIndex().row() model = self._listView.model() if row > 0: items = model.takeRow(row) model.insertRow(row - 1, items) self._listView.setCurrentIndex(model.index(row - 1, 0)) def _onDownAction(self): row = self._listView.currentIndex().row() model = self._listView.model() if row < model.rowCount() and row >= 0: items = model.takeRow(row) if row == model.rowCount(): model.appendRow(items) else: model.insertRow(row + 1, items) self._listView.setCurrentIndex(model.index(row + 1, 0)) def setModel(self, model): """ Set a model to select items from """ self._model = model self._listView.setItemDelegate(self._MyItemDelegate(self._model, self)) def addItem(self, *args): """ Add a new entry in the list """ item = QStandardItem(*args) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listView.model().appendRow(item) def setItems(self, items): self._listView.model().clear() for item in items: self.addItem(item) def items(self): order = [] for row in range(self._listView.model().rowCount()): order.append(str(self._listView.model().item(row, 0).text())) return order def __changed(self): self.sortingOrderChanged.emit() sortingOrder = property(items, setItems)
class OWPythonScript(OWWidget): name = "Pipeline Console" description = "Console of pipeline." icon = "icons/PythonScript.svg" priority = 3150 keywords = ["build", "run"] class Inputs: data = Input("Data", Table, replaces=["in_data"], default=True, multiple=True) learner = Input("Learner", Learner, replaces=["in_learner"], default=True, multiple=True) classifier = Input("Classifier", Model, replaces=["in_classifier"], default=True, multiple=True) object = Input("Object", object, replaces=["in_object"], default=False, multiple=True) # Pipeline structure pipline_in = Input("Input info", list) class Outputs: data = Output("Data", Table, replaces=["out_data"]) learner = Output("Learner", Learner, replaces=["out_learner"]) classifier = Output("Classifier", Model, replaces=["out_classifier"]) object = Output("Object", object, replaces=["out_object"]) # JJ @Inputs.pipline_in def set_pipline_in(self, pipline_in): if pipline_in is not None: # self.infoa.setText("There is input, which is the beginning of th pipeline.") self.pipeline_wrapping(pipline_in) # self.pipeline_writing() ########## Only for test # self.read() ########## Only for test # else: # self.infoa.setText("No data on input yet, waiting to get something.") signal_names = ("data", "learner", "classifier", "object") settings_version = 2 scriptLibrary: 'List[_ScriptData]' = Setting([{ "name": "Hello world", "script": "print('Hello world')\n", "filename": None }]) currentScriptIndex = Setting(0) scriptText: Optional[str] = Setting(None, schema_only=True) splitterState: Optional[bytes] = Setting(None) # Widgets in the same schema share namespace through a dictionary whose # key is self.signalManager. ales-erjavec expressed concern (and I fully # agree!) about widget being aware of the outside world. I am leaving this # anyway. If this causes any problems in the future, replace this with # shared_namespaces = {} and thus use a common namespace for all instances # of # PythonScript even if they are in different schemata. shared_namespaces = defaultdict(dict) class Error(OWWidget.Error): pass def __init__(self): super().__init__() self.libraryListSource = [] for name in self.signal_names: setattr(self, name, {}) self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + "<li>".join(map("in_{0}, in_{0}s".format, self.signal_names)) + "</ul></p><p>Output variables:<ul><li>" + "<li>".join(map("out_{0}".format, self.signal_names)) + "</ul></p>") self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) restore_saved = QAction("Undo Changes to Selected Script", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) restore_saved.triggered.connect(self.restoreSaved) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) menu.addAction(restore_saved) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.button(self.controlArea, self, 'Run Script', callback=self.commit) self.build_pipeline_button = gui.button(self.controlArea, self, 'Build Pipeline', callback=self.build_pipeline) self.run_pipeline_button = gui.button(self.controlArea, self, 'Run Pipeline', callback=self.run_pipeline) run = QAction("Run script", self, triggered=self.commit, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R)) self.addAction(run) self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.vBox(self, 'Python Script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) self.splitCanvas.setSizes([2, 1]) self.setAcceptDrops(True) self.controlArea.layout().addStretch(10) self._restoreState() self.settingsAboutToBePacked.connect(self._saveState) # pipeline infomation self.hyperparameter = {} self.python_path = "ENDING" self.primitive_info = PrimitiveInfo(python_path=self.python_path, id=-1, hyperparameter=self.hyperparameter, ancestors={}) def sizeHint(self) -> QSize: return super().sizeHint().expandedTo(QSize(800, 600)) def _restoreState(self): self.libraryListSource = [ Script.fromdict(s) for s in self.scriptLibrary ] self.libraryList.wrap(self.libraryListSource) select_row(self.libraryView, self.currentScriptIndex) if self.scriptText is not None: current = self.text.toPlainText() # do not mark scripts as modified if self.scriptText != current: self.text.document().setPlainText(self.scriptText) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) def _saveState(self): self.scriptLibrary = [s.asdict() for s in self.libraryListSource] self.scriptText = self.text.toPlainText() self.splitterState = bytes(self.splitCanvas.saveState()) def handle_input(self, obj, sig_id, signal): sig_id = sig_id[0] dic = getattr(self, signal) if obj is None: if sig_id in dic.keys(): del dic[sig_id] else: dic[sig_id] = obj @Inputs.data def set_data(self, data, sig_id): self.handle_input(data, sig_id, "data") @Inputs.learner def set_learner(self, data, sig_id): self.handle_input(data, sig_id, "learner") @Inputs.classifier def set_classifier(self, data, sig_id): self.handle_input(data, sig_id, "classifier") @Inputs.object def set_object(self, data, sig_id): self.handle_input(data, sig_id, "object") def handleNewSignals(self): self.commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *_): self.libraryList.append( Script("New script", self.text.toPlainText(), 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *_): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)') if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *_): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *_): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, _deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if not isinstance(script, Script): script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *_): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def restoreSaved(self): index = self.selectedScriptIndex() if index is not None: self.text.document().setPlainText(self.libraryList[index].script) self.text.document().setModified(False) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)') if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = self.shared_namespaces[self.signalManager].copy() for name in self.signal_names: value = getattr(self, name) all_values = list(value.values()) one_value = all_values[0] if len(all_values) == 1 else None d["in_" + name + "s"] = all_values d["in_" + name] = one_value return d def update_namespace(self, namespace): not_saved = reduce(set.union, ({f"in_{name}s", f"in_{name}", f"out_{name}"} for name in self.signal_names)) self.shared_namespaces[self.signalManager].update({ name: value for name, value in namespace.items() if name not in not_saved }) def commit(self): self.Error.clear() lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) self.update_namespace(self.console.locals) for signal in self.signal_names: out_var = self.console.locals.get("out_" + signal) signal_type = getattr(self.Outputs, signal).type if not isinstance(out_var, signal_type) and out_var is not None: self.Error.add_message( signal, "'{}' has to be an instance of '{}'.".format( signal, signal_type.__name__)) getattr(self.Error, signal)() out_var = None getattr(self.Outputs, signal).send(out_var) def dragEnterEvent(self, event): # pylint: disable=no-self-use urls = event.mimeData().urls() if urls: # try reading the file as text c = read_file_content(urls[0].toLocalFile(), limit=1000) if c is not None: event.acceptProposedAction() def dropEvent(self, event): """Handle file drops""" urls = event.mimeData().urls() if urls: self.text.pasteFile(urls[0]) @classmethod def migrate_settings(cls, settings, version): if version is not None and version < 2: scripts = settings.pop("libraryListSource") # type: List[Script] library = [ dict(name=s.name, script=s.script, filename=s.filename) for s in scripts ] # type: List[_ScriptData] settings["scriptLibrary"] = library def run_pipeline(self): print('run_pipeline') run_pipeline(self.output_list_ending, stdout=self.console) self.console.new_prompt(sys.ps1) # flush the console pass def build_pipeline(self): print('build_pipeline') build_pipeline(self.output_list_ending, self.primitive_mapping, stdout=self.console) self.console.new_prompt(sys.ps1) # flush the console pass def pipeline_wrapping(self, pipline_in): self.output_list = pipline_in[0] self.ancestors_path = pipline_in[1] self.primitive_info.ancestors['inputs'] = self.ancestors_path self.output_list_ending = self.output_list + [self.primitive_info] self.primitive_mapping = {} for i in range(0, len(self.output_list_ending)): self.primitive_mapping[self.output_list_ending[i].id] = i
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 inputs = [("Data", Orange.data.Table, "set_data"), ("Learner", Learner, "set_learner")] outputs = [("Data", Orange.data.Table)] DEFAULT_LEARNER = SimpleTreeLearner() METHODS = [AsDefault(), impute.DoNotImpute(), impute.Average(), impute.AsValue(), impute.Model(DEFAULT_LEARNER), impute.Random(), impute.DropInstances(), impute.Default()] DEFAULT, DO_NOT_IMPUTE, MODEL_BASED_IMPUTER, AS_INPUT = 0, 1, 4, 7 settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(DO_NOT_IMPUTE) variable_methods = settings.ContextSetting({}) autocommit = settings.Setting(False) default_value = settings.Setting(0.) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for i, method in enumerate(self.METHODS): if not method.columns_only: button = QRadioButton(method.name) button.setChecked(i == self.default_method_index) button_group.addButton(button, i) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView( selectionMode=QListView.ExtendedSelection ) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed ) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for i, method in enumerate(self.METHODS): button = QRadioButton(text=method.name) button_group.addButton(button, i) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected ) self.value_combo.currentIndexChanged.connect(self._on_value_changed) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, value=self.default_value ) self.value_stack = value_stack = QStackedLayout() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addLayout(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection ) method_layout.addStretch(2) reset_button = QPushButton( "Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_methods, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit( self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 80) box.layout().insertWidget(0, self.report_button) self.data = None self.modified = False self.default_method = self.METHODS[self.default_method_index] self.update_varview() @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: self._default_method_index = index self.default_button_group.button(index).setChecked(True) self.default_method = self.METHODS[self.default_method_index] self.METHODS[self.DEFAULT].method = self.default_method # update variable view for index in map(self.varmodel.index, range(len(self.varmodel))): self.varmodel.setData(index, self.variable_methods.get(index.row(), self.METHODS[self.DEFAULT]), Qt.UserRole) self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self.variable_methods = {} self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) self.update_varview() self.unconditional_commit() def set_learner(self, learner): self.learner = learner or self.DEFAULT_LEARNER imputer = self.METHODS[self.MODEL_BASED_IMPUTER] imputer.learner = self.learner button = self.default_button_group.button(self.MODEL_BASED_IMPUTER) button.setText(imputer.name) variable_button = self.variable_button_group.button(self.MODEL_BASED_IMPUTER) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = self.MODEL_BASED_IMPUTER self.commit() def get_method_for_column(self, column_index): """Returns the imputation method for column by its index. """ if not isinstance(column_index, int): column_index = column_index.row() return self.variable_methods.get(column_index, self.METHODS[self.DEFAULT]) def _invalidate(self): self.modified = True self.commit() def commit(self): data = self.data if self.data is not None: if not len(self.data): self.send("Data", self.data) self.modified = False return drop_mask = np.zeros(len(self.data), bool) attributes = [] class_vars = [] self.warning() with self.progressBar(len(self.varmodel)) as progress: for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, self.default_method) if not method.supports_variable(var): self.warning("Default method can not handle '{}'". format(var.name)) elif isinstance(method, impute.DropInstances): drop_mask |= method(self.data, var) else: var = method(self.data, var) if isinstance(var, Orange.data.Variable): var = [var] if i < len(self.data.domain.attributes): attributes.extend(var) else: class_vars.extend(var) progress.advance() domain = Orange.data.Domain(attributes, class_vars, self.data.domain.metas) data = self.data.from_table(domain, self.data[~drop_mask]) self.send("Data", data) self.modified = False def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, None) if method is not None: specific.append("{} ({})".format(var.name, str(method))) default = self.default_method.name if specific: self.report_items(( ("Default method", default), ("Specific imputers", ", ".join(specific)) )) else: self.report_items((("Method", default),)) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() methods = set(self.get_method_for_column(i.row()).name for i in indexes) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) if len(methods) == 1: method = methods.pop() for i, m in enumerate(self.METHODS): if method == m.name: self.variable_button_group.button(i).setChecked(True) elif self.variable_button_group.checkedButton() is not None: self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) for method, button in zip(self.METHODS, self.variable_button_group.buttons()): enabled = all(method.supports_variable(var) for var in selected_vars) button.setEnabled(enabled) if not has_discrete: self.value_stack.setEnabled(True) self.value_stack.setCurrentWidget(self.value_double) self._on_value_changed() elif len(selected_vars) == 1: self.value_stack.setEnabled(True) self.value_stack.setCurrentWidget(self.value_combo) self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) self._on_value_changed() else: self.variable_button_group.button(self.AS_INPUT).setEnabled(False) self.value_stack.setEnabled(False) def set_method_for_current_selection(self, method_index): indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): if method_index == self.DEFAULT: for index in indexes: self.variable_methods.pop(index, None) else: method = self.METHODS[method_index].copy() for index in indexes: self.variable_methods[index.row()] = method self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), Qt.UserRole) def _on_value_selected(self): self.variable_button_group.button(self.AS_INPUT).setChecked(True) self._on_value_changed() def _on_value_changed(self): widget = self.value_stack.currentWidget() if widget is self.value_combo: value = self.value_combo.currentText() else: value = self.value_double.value() self.default_value = value self.METHODS[self.AS_INPUT].default = value index = self.variable_button_group.checkedId() if index == self.AS_INPUT: self.set_method_for_current_selection(index) def reset_variable_methods(self): indexes = map(self.varmodel.index, range(len(self.varmodel))) self.set_method_for_indexes(indexes, self.DEFAULT) self.variable_button_group.button(self.DEFAULT).setChecked(True)
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 keywords = ["substitute", "missing"] class Inputs: data = Input("Data", Orange.data.Table) learner = Input("Learner", Learner) class Outputs: data = Output("Data", Orange.data.Table) class Error(OWWidget.Error): imputation_failed = Msg("Imputation failed for '{}'") model_based_imputer_sparse = \ Msg("Model based imputer does not work for sparse data") class Warning(OWWidget.Warning): cant_handle_var = Msg("Default method can not handle '{}'") settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(int(Method.Leave)) # type: int # Per-variable imputation state (synced in storeSpecificSettings) _variable_imputation_state = settings.ContextSetting( {}) # type: VariableState autocommit = settings.Setting(True) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() self.data = None # type: Optional[Orange.data.Table] self.learner = None # type: Optional[Learner] self.default_learner = SimpleTreeLearner() self.modified = False self.executor = qconcurrent.ThreadExecutor(self) self.__task = None main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for method, _ in list(METHODS.items())[1:-1]: imputer = self.create_imputer(method) button = QRadioButton(imputer.name) button.setChecked(method == self.default_method_index) button_group.addButton(button, method) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView(selectionMode=QListView.ExtendedSelection, uniformItemSizes=True) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for method in Method: imputer = self.create_imputer(method) button = QRadioButton(text=imputer.name) button_group.addButton(button, method) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, ) self.value_stack = value_stack = QStackedWidget() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addWidget(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection) method_layout.addStretch(2) reset_button = QPushButton("Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_state, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_apply(self.controlArea, self, "autocommit") box.button.setFixedWidth(180) box.layout().insertStretch(0) self.info.set_input_summary(self.info.NoInput) self.info.set_output_summary(self.info.NoOutput) def create_imputer(self, method, *args): # type: (Method, ...) -> impute.BaseImputeMethod if method == Method.Model: if self.learner is not None: return impute.Model(self.learner) else: return impute.Model(self.default_learner) elif method == Method.AsAboveSoBelow: assert self.default_method_index != Method.AsAboveSoBelow default = self.create_imputer(Method(self.default_method_index)) m = AsDefault() m.method = default return m else: return METHODS[method](*args) @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: assert index != Method.AsAboveSoBelow self._default_method_index = index self.default_button_group.button(index).setChecked(True) # update variable view self.update_varview() self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @Inputs.data @check_sql_input def set_data(self, data): self.cancel() self.closeContext() self.varmodel[:] = [] self._variable_imputation_state = {} # type: VariableState self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) # restore per variable imputation state self._restore_state(self._variable_imputation_state) summary = len(data) if data else self.info.NoInput details = format_summary_details(data) if data else "" self.info.set_input_summary(summary, details) self.update_varview() self.unconditional_commit() @Inputs.learner def set_learner(self, learner): self.cancel() self.learner = learner or self.default_learner imputer = self.create_imputer(Method.Model) button = self.default_button_group.button(Method.Model) button.setText(imputer.name) variable_button = self.variable_button_group.button(Method.Model) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = Method.Model self.update_varview() self.commit() def get_method_for_column(self, column_index): # type: (int) -> impute.BaseImputeMethod """ Return the imputation method for column by its index. """ assert 0 <= column_index < len(self.varmodel) idx = self.varmodel.index(column_index, 0) state = idx.data(StateRole) if state is None: state = (Method.AsAboveSoBelow, ()) return self.create_imputer(state[0], *state[1]) def _invalidate(self): self.modified = True if self.__task is not None: self.cancel() self.commit() def commit(self): self.cancel() self.warning() self.Error.imputation_failed.clear() self.Error.model_based_imputer_sparse.clear() summary = len(self.data) if self.data else self.info.NoOutput detail = format_summary_details(self.data) if self.data else "" self.info.set_output_summary(summary, detail) if not self.data or not self.varmodel.rowCount(): self.Outputs.data.send(self.data) self.modified = False return data = self.data impute_state = [(i, var, self.get_method_for_column(i)) for i, var in enumerate(self.varmodel)] # normalize to the effective method bypasing AsDefault impute_state = [(i, var, m.method if isinstance(m, AsDefault) else m) for i, var, m in impute_state] def impute_one(method, var, data): # Readability counts, pylint: disable=no-else-raise # type: (impute.BaseImputeMethod, Variable, Table) -> Any if isinstance(method, impute.Model) and data.is_sparse(): raise SparseNotSupported() elif isinstance(method, impute.DropInstances): return RowMask(method(data, var)) elif not method.supports_variable(var): raise VariableNotSupported(var) else: return method(data, var) futures = [] for _, var, method in impute_state: f = self.executor.submit(impute_one, copy.deepcopy(method), var, data) futures.append(f) w = qconcurrent.FutureSetWatcher(futures) w.doneAll.connect(self.__commit_finish) w.progressChanged.connect(self.__progress_changed) self.__task = Task(futures, w) self.progressBarInit() self.setInvalidated(True) @Slot() def __commit_finish(self): assert QThread.currentThread() is self.thread() assert self.__task is not None futures = self.__task.futures assert len(futures) == len(self.varmodel) assert self.data is not None def get_variable(variable, future, drop_mask) \ -> Optional[List[Orange.data.Variable]]: # Returns a (potentially empty) list of variables, # or None on failure that should interrupt the imputation assert future.done() try: res = future.result() except SparseNotSupported: self.Error.model_based_imputer_sparse() return [] # None? except VariableNotSupported: self.Warning.cant_handle_var(variable.name) return [] except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error for %s", variable.name, exc_info=True) self.Error.imputation_failed(variable.name) return None if isinstance(res, RowMask): drop_mask |= res.mask newvar = variable else: newvar = res if isinstance(newvar, Orange.data.Variable): newvar = [newvar] return newvar def create_data(attributes, class_vars): domain = Orange.data.Domain(attributes, class_vars, self.data.domain.metas) try: return self.data.from_table(domain, self.data[~drop_mask]) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error", exc_info=True) self.Error.imputation_failed("Unknown") return None self.__task = None self.setInvalidated(False) self.progressBarFinished() attributes = [] class_vars = [] drop_mask = np.zeros(len(self.data), bool) for i, (var, fut) in enumerate(zip(self.varmodel, futures)): newvar = get_variable(var, fut, drop_mask) if newvar is None: data = None break if i < len(self.data.domain.attributes): attributes.extend(newvar) else: class_vars.extend(newvar) else: data = create_data(attributes, class_vars) self.Outputs.data.send(data) self.modified = False summary = len(data) if data else self.info.NoOutput details = format_summary_details(data) if data else "" self.info.set_output_summary(summary, details) @Slot(int, int) def __progress_changed(self, n, d): assert QThread.currentThread() is self.thread() assert self.__task is not None self.progressBarSet(100. * n / d) def cancel(self): self.__cancel(wait=False) def __cancel(self, wait=False): if self.__task is not None: task, self.__task = self.__task, None task.cancel() task.watcher.doneAll.disconnect(self.__commit_finish) task.watcher.progressChanged.disconnect(self.__progress_changed) if wait: concurrent.futures.wait(task.futures) task.watcher.flush() self.progressBarFinished() self.setInvalidated(False) def onDeleteWidget(self): self.__cancel(wait=True) super().onDeleteWidget() def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.get_method_for_column(i) if not isinstance(method, AsDefault): specific.append("{} ({})".format(var.name, str(method))) default = self.create_imputer(Method.AsAboveSoBelow) if specific: self.report_items((("Default method", default.name), ("Specific imputers", ", ".join(specific)))) else: self.report_items((("Method", default.name), )) def _on_var_selection_changed(self): # Method is well documented, splitting it is not needed for readability, # thus pylint: disable=too-many-branches indexes = self.selection.selectedIndexes() defmethod = (Method.AsAboveSoBelow, ()) methods = [index.data(StateRole) for index in indexes] methods = [m if m is not None else defmethod for m in methods] methods = set(methods) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) fixed_value = None value_stack_enabled = False current_value_widget = None if len(methods) == 1: method_type, parameters = methods.pop() for m in Method: if method_type == m: self.variable_button_group.button(m).setChecked(True) if method_type == Method.Default: (fixed_value, ) = parameters elif self.variable_button_group.checkedButton() is not None: # Uncheck the current button self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) assert self.variable_button_group.checkedButton() is None # Update variable methods GUI enabled state based on selection. for method in Method: # use a default constructed imputer to query support imputer = self.create_imputer(method) enabled = all( imputer.supports_variable(var) for var in selected_vars) button = self.variable_button_group.button(method) button.setEnabled(enabled) # Update the "Value" edit GUI. if not has_discrete: # no discrete variables -> allow mass edit for all (continuous vars) value_stack_enabled = True current_value_widget = self.value_double elif len(selected_vars) == 1: # single discrete var -> enable and fill the values combo value_stack_enabled = True current_value_widget = self.value_combo self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) else: # mixed type selection -> disable value_stack_enabled = False current_value_widget = None self.variable_button_group.button(Method.Default).setEnabled(False) self.value_stack.setEnabled(value_stack_enabled) if current_value_widget is not None: self.value_stack.setCurrentWidget(current_value_widget) if fixed_value is not None: # set current value if current_value_widget is self.value_combo: self.value_combo.setCurrentIndex(fixed_value) elif current_value_widget is self.value_double: self.value_double.setValue(fixed_value) else: assert False def set_method_for_current_selection(self, method_index): # type: (Method) -> None indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): # type: (List[QModelIndex], Method) -> None if method_index == Method.AsAboveSoBelow: for index in indexes: self.varmodel.setData(index, None, StateRole) elif method_index == Method.Default: current = self.value_stack.currentWidget() if current is self.value_combo: value = self.value_combo.currentIndex() else: value = self.value_double.value() for index in indexes: state = (int(Method.Default), (value, )) self.varmodel.setData(index, state, StateRole) else: state = (int(method_index), ()) for index in indexes: self.varmodel.setData(index, state, StateRole) self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), DisplayMethodRole) def _on_value_selected(self): # The fixed 'Value' in the widget has been changed by the user. self.variable_button_group.button(Method.Default).setChecked(True) self.set_method_for_current_selection(Method.Default) def reset_variable_state(self): indexes = list(map(self.varmodel.index, range(len(self.varmodel)))) self.set_method_for_indexes(indexes, Method.AsAboveSoBelow) self.variable_button_group.button( Method.AsAboveSoBelow).setChecked(True) def _store_state(self): # type: () -> VariableState """ Save the current variable imputation state """ state = {} # type: VariableState for i, var in enumerate(self.varmodel): index = self.varmodel.index(i) m = index.data(StateRole) if m is not None: state[var_key(var)] = m return state def _restore_state(self, state): # type: (VariableState) -> None """ Restore the variable imputation state from the saved state """ def check(state): # check if state is a proper State if isinstance(state, tuple) and len(state) == 2: m, p = state if isinstance(m, int) and isinstance(p, tuple) and \ 0 <= m < len(Method): return True return False for i, var in enumerate(self.varmodel): m = state.get(var_key(var), None) if check(m): self.varmodel.setData(self.varmodel.index(i), m, StateRole) def storeSpecificSettings(self): self._variable_imputation_state = self._store_state() super().storeSpecificSettings()
class OWPythonScript(widget.OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." icon = "icons/PythonScript.svg" priority = 3150 keywords = ["file", "program"] class Inputs: data = Input("Data", Table, replaces=["in_data"], default=True, multiple=True) learner = Input("Learner", Learner, replaces=["in_learner"], default=True, multiple=True) classifier = Input("Classifier", Model, replaces=["in_classifier"], default=True, multiple=True) object = Input("Object", object, replaces=["in_object"], default=False, multiple=True) class Outputs: data = Output("Data", Table, replaces=["out_data"]) learner = Output("Learner", Learner, replaces=["out_learner"]) classifier = Output("Classifier", Model, replaces=["out_classifier"]) object = Output("Object", object, replaces=["out_object"]) signal_names = ("data", "learner", "classifier", "object") settingsHandler = PrepareSavingSettingsHandler() libraryListSource = \ Setting([Script("Hello world", "print('Hello world')\n")]) currentScriptIndex = Setting(0) scriptText = Setting(None, schema_only=True) splitterState = Setting(None) class Error(OWWidget.Error): pass def __init__(self): super().__init__() for name in self.signal_names: setattr(self, name, {}) for s in self.libraryListSource: s.flags = 0 self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + "<li>".join(map("in_{0}, in_{0}s".format, self.signal_names)) + "</ul></p><p>Output variables:<ul><li>" + "<li>".join(map("out_{0}".format, self.signal_names)) + "</ul></p>" ) self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) ) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged ) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) restore_saved = QAction("Undo Changes to Selected Script", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) restore_saved.triggered.connect(self.restoreSaved) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) menu.addAction(restore_saved) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.button(self.controlArea, self, 'Run', callback=self.commit) self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.vBox(self, 'Python Script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) select_row(self.libraryView, self.currentScriptIndex) self.restoreScriptText() self.splitCanvas.setSizes([2, 1]) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) def storeSpecificSettings(self): self.saveScriptText() def restoreScriptText(self): if self.scriptText is not None: current = self.text.toPlainText() # do not mark scripts as modified if self.scriptText != current: self.text.document().setPlainText(self.scriptText) def saveScriptText(self): self.scriptText = self.text.toPlainText() def handle_input(self, obj, id, signal): id = id[0] dic = getattr(self, signal) if obj is None: if id in dic.keys(): del dic[id] else: dic[id] = obj @Inputs.data def set_data(self, data, id): self.handle_input(data, id, "data") @Inputs.learner def set_learner(self, data, id): self.handle_input(data, id, "learner") @Inputs.classifier def set_classifier(self, data, id): self.handle_input(data, id, "classifier") @Inputs.object def set_object(self, data, id): self.handle_input(data, id, "object") def handleNewSignals(self): self.commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *args): self.libraryList.append(Script("New script", self.text.toPlainText(), 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *args): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)' ) if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *args): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *args): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if type(script) != Script: script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *args): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def onSpliterMoved(self, pos, ind): self.splitterState = bytes(self.splitCanvas.saveState()) def restoreSaved(self): index = self.selectedScriptIndex() if index is not None: self.text.document().setPlainText(self.libraryList[index].script) self.text.document().setModified(False) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)' ) if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = {} for name in self.signal_names: value = getattr(self, name) all_values = list(value.values()) one_value = all_values[0] if len(all_values) == 1 else None d["in_" + name + "s"] = all_values d["in_" + name] = one_value return d def commit(self): self.Error.clear() self._script = str(self.text.toPlainText()) lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for signal in self.signal_names: out_var = self.console.locals.get("out_" + signal) signal_type = getattr(self.Outputs, signal).type if not isinstance(out_var, signal_type) and out_var is not None: self.Error.add_message(signal, "'{}' has to be an instance of '{}'.". format(signal, signal_type.__name__)) getattr(self.Error, signal)() out_var = None getattr(self.Outputs, signal).send(out_var)
class OWWordList(OWWidget): name = "Word List" description = "Create a list of words." icon = "icons/WordList.svg" priority = 1000 class Inputs: words = Input("Words", Table) class Outputs: selected_words = Output("Selected Words", Table) words = Output("Words", Table, dynamic=False) class Warning(OWWidget.Warning): no_string_vars = Msg("Input needs at least one Text variable.") NONE, CACHED, LIBRARY = range(3) # library list modification types want_main_area = False resizing_enabled = True settingsHandler = DomainContextHandler() word_list_library: List[Dict] = Setting([ { "name": WordList.generate_word_list_name([]), "words": [] }, ]) word_list_index: int = Setting(0) words_var: Optional[StringVariable] = ContextSetting(None) update_rule_index: int = Setting(UpdateRules.INTERSECT) words: List[str] = Setting(None, schema_only=True) selected_words: Set[str] = Setting(set(), schema_only=True) def __init__(self): super().__init__(self) flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable self.library_model = PyListModel([], self, flags=flags) self.words_model = PyListModel([], self, flags=flags, enable_dnd=True) self.library_view: QListView = None self.words_view: ListView = None self.__input_words_model = DomainModel(valid_types=(StringVariable, )) self.__input_words: Optional[Table] = None self.__library_box: QGroupBox = gui.vBox(None, "Library") self.__input_box: QGroupBox = gui.vBox(None, "Input") self.__words_box: QGroupBox = gui.vBox(None, box=True) self.__update_rule_rb: QRadioButton = None self.__add_word_action: QAction = None self.__remove_word_action: QAction = None self._setup_gui() self._restore_state() self.settingsAboutToBePacked.connect(self._save_state) def _setup_gui(self): layout = QGridLayout() gui.widgetBox(self.controlArea, orientation=layout) self._setup_library_box() self._setup_input_box() self._setup_words_box() layout.addWidget(self.__library_box, 0, 0) layout.addWidget(self.__input_box, 1, 0) layout.addWidget(self.__words_box, 0, 1, 0, 1) def _setup_library_box(self): self.library_view = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding), ) self.library_view.setItemDelegate(WordListItemDelegate(self)) self.library_view.setModel(self.library_model) self.library_view.selectionModel().selectionChanged.connect( self.__on_library_selection_changed) self.__library_box.layout().setSpacing(1) self.__library_box.layout().addWidget(self.library_view) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) action = QAction("+", self) action.setToolTip("Add a new word list to the library") action.triggered.connect(self.__on_add_word_list) actions_widget.addAction(action) action = QAction("\N{MINUS SIGN}", self) action.setToolTip("Remove word list from library") action.triggered.connect(self.__on_remove_word_list) actions_widget.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.__on_update_word_list) actions_widget.addAction(action) gui.rubber(actions_widget.layout()) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Words from File", self) new_from_file.triggered.connect(self.__on_import_word_list) save_to_file = QAction("Save Words to File", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) save_to_file.triggered.connect(self.__on_save_word_list) menu = QMenu(actions_widget) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = actions_widget.addAction(action) button.setPopupMode(QToolButton.InstantPopup) self.__library_box.layout().addWidget(actions_widget) def __on_library_selection_changed(self, selected: QItemSelection, *_): index = [i.row() for i in selected.indexes()] if index: current = index[0] word_list: WordList = self.library_model[current] self.word_list_index = current self.selected_words = set() self.words_model.wrap(list(word_list.cached_words)) self._apply_update_rule() def __on_add_word_list(self): taken = [l.name for l in self.library_model] name = WordList.generate_word_list_name(taken) word_list = WordList(name, self.words_model[:]) self.library_model.append(word_list) self._set_selected_word_list(len(self.library_model) - 1) def __on_remove_word_list(self): index = self._get_selected_word_list_index() if index is not None: del self.library_model[index] self._set_selected_word_list(max(index - 1, 0)) self._apply_update_rule() def __on_update_word_list(self): self._set_word_list_modified(mod_type=self.LIBRARY) def __on_import_word_list(self): filename, _ = QFileDialog.getOpenFileName( self, "Open Word List", os.path.expanduser("~/"), "Text files (*.txt)\nAll files(*.*)") if filename: name = os.path.basename(filename) with open(filename, encoding="utf-8") as f: words = [line.strip() for line in f.readlines()] self.library_model.append(WordList(name, words, filename=filename)) self._set_selected_word_list(len(self.library_model) - 1) self._apply_update_rule() def __on_save_word_list(self): index = self._get_selected_word_list_index() if index is not None: word_list = self.library_model[index] filename = word_list.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, "Save Word List", filename, "Text files (*.txt)\nAll files(*.*)") if filename: head, tail = os.path.splitext(filename) if not tail: filename = head + ".txt" with open(filename, "w", encoding="utf-8") as f: for word in self.words_model: f.write(f"{word}\n") def _setup_input_box(self): gui.comboBox(self.__input_box, self, "words_var", label="Word variable:", orientation=Qt.Vertical, model=self.__input_words_model, callback=self._apply_update_rule) gui.radioButtons(self.__input_box, self, "update_rule_index", UpdateRules.ITEMS, label="Update: ", orientation=Qt.Vertical, callback=self.__on_update_rule_changed) self.__input_box.setEnabled(False) def __on_update_rule_changed(self): self._enable_words_actions() self._apply_update_rule() def _setup_words_box(self): self.words_view = ListView() self.words_view.drop_finished.connect(self.__on_words_data_changed) self.words_view.setModel(self.words_model) self.words_view.selectionModel().selectionChanged.connect( self.__on_words_selection_changed) self.words_model.dataChanged.connect(self.__on_words_data_changed) self.__words_box.layout().setSpacing(1) self.__words_box.layout().addWidget(self.words_view) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) action = QAction("+", self.words_view, toolTip="Add a new word") action.triggered.connect(self.__on_add_word) actions_widget.addAction(action) self.__add_word_action = action action = QAction("\N{MINUS SIGN}", self, toolTip="Remove word") action.triggered.connect(self.__on_remove_word) actions_widget.addAction(action) self.__remove_word_action = action gui.rubber(actions_widget) action = QAction("Sort", self) action.setToolTip("Sort words alphabetically") action.triggered.connect(self.__on_apply_sorting) actions_widget.addAction(action) self.__words_box.layout().addWidget(actions_widget) def __on_words_data_changed(self): self._set_word_list_modified(mod_type=self.CACHED) self.commit() def __on_words_selection_changed(self): self.commit() def __on_add_word(self): row = self.words_model.rowCount() if not self.words_model.insertRow(self.words_model.rowCount()): return with disconnected(self.words_view.selectionModel().selectionChanged, self.__on_words_selection_changed): self._set_selected_words([0]) index = self.words_model.index(row, 0) self.words_view.setCurrentIndex(index) self.words_model.setItemData(index, {Qt.EditRole: ""}) self.words_view.edit(index) def __on_remove_word(self): rows = self.words_view.selectionModel().selectedRows(0) if not rows: return indices = sorted([row.row() for row in rows], reverse=True) with disconnected(self.words_view.selectionModel().selectionChanged, self.__on_words_selection_changed): for index in indices: self.words_model.removeRow(index) if self.words_model: self._set_selected_words([max(0, indices[-1] - 1)]) self.__on_words_data_changed() def __on_apply_sorting(self): if not self.words_model: return words = self.words_model[:] mask = np.zeros(len(words), dtype=bool) selection = self._get_selected_words_indices() if selection: mask[selection] = True indices = np.argsort(words) self.words_model.wrap([words[i] for i in indices]) self._set_word_list_modified(mod_type=self.CACHED) if selection: self._set_selected_words(list(np.flatnonzero(mask[indices]))) else: self.commit() @Inputs.words def set_words(self, words: Optional[Table]): self.closeContext() self.__input_words = words self._check_input_words() self._init_controls() self.openContext(self.__input_words) self._apply_update_rule() def _check_input_words(self): self.Warning.no_string_vars.clear() if self.__input_words: metas = self.__input_words.domain.metas if not any(isinstance(m, StringVariable) for m in metas): self.Warning.no_string_vars() self.__input_words = None def _init_controls(self): words = self.__input_words domain = words.domain if words is not None else None self.__input_words_model.set_domain(domain) if len(self.__input_words_model) > 0: self.words_var = self.__input_words_model[0] self.__input_box.setEnabled(bool(self.__input_words_model)) self._enable_words_actions() def _enable_words_actions(self): if bool(self.__input_words_model) \ and self.update_rule_index != UpdateRules.LIBRARY: self.words_view.setEditTriggers(QListView.NoEditTriggers) self.__add_word_action.setEnabled(False) self.__remove_word_action.setEnabled(False) else: self.words_view.setEditTriggers(QListView.DoubleClicked | QListView.EditKeyPressed) self.__add_word_action.setEnabled(True) self.__remove_word_action.setEnabled(True) def _apply_update_rule(self): lib_index = self._get_selected_word_list_index() lib_words, in_words, update_rule = [], [], UpdateRules.LIBRARY if lib_index is not None: lib_words = self.library_model[lib_index].cached_words else: lib_words = self.words_model[:] if self.__input_words is not None: in_words = self.__input_words.get_column_view(self.words_var)[0] in_words = list(in_words) update_rule = self.update_rule_index UpdateRules.update(self.words_model, lib_words, in_words, update_rule) if lib_index is not None: cached = self.library_model[lib_index].cached_words modified = WordList.NotModified if cached == self.words_model[:] \ else WordList.Modified self.library_model[lib_index].update_rule_flag = modified self._set_word_list_modified(mod_type=self.NONE) self.library_view.repaint() # Apply selection. selection_changed invokes commit(). # If there is no selection, call commit explicitly. if any(w in self.selected_words for w in self.words_model): self.set_selected_words() self.words_view.repaint() else: self.commit() def commit(self): selection = self._get_selected_words_indices() self.selected_words = set(np.array(self.words_model)[selection]) words, selected_words = None, None if self.words_model: _words = create_words_table(self.words_model) if selection: selected_words = _words[selection] words = create_annotated_table(_words, selection) self.Outputs.words.send(words) self.Outputs.selected_words.send(selected_words) def _set_word_list_modified(self, mod_type): index = self._get_selected_word_list_index() if index is not None: if mod_type == self.LIBRARY: self.library_model[index].words = self.words_model[:] self.library_model[index].cached_words = self.words_model[:] self.library_model[index].update_rule_flag \ = WordList.NotModified elif mod_type == self.CACHED: self.library_model[index].cached_words = self.words_model[:] elif mod_type == self.NONE: pass else: raise NotImplementedError self.library_model.emitDataChanged(index) self.library_view.repaint() def _set_selected_word_list(self, index: int): sel_model: QItemSelectionModel = self.library_view.selectionModel() sel_model.select(self.library_model.index(index, 0), QItemSelectionModel.ClearAndSelect) def _get_selected_word_list_index(self) -> Optional[int]: rows = self.library_view.selectionModel().selectedRows() return rows[0].row() if rows else None def _set_selected_words(self, indices: List[int]): selection = QItemSelection() sel_model: QItemSelectionModel = self.words_view.selectionModel() for i in indices: selection.append(QItemSelectionRange(self.words_model.index(i, 0))) sel_model.select(selection, QItemSelectionModel.ClearAndSelect) def _get_selected_words_indices(self) -> List[int]: rows = self.words_view.selectionModel().selectedRows() return [row.row() for row in rows] def set_selected_words(self): if self.selected_words: indices = [ i for i, w in enumerate(self.words_model) if w in self.selected_words ] self._set_selected_words(indices) def _restore_state(self): source = [WordList.from_dict(s) for s in self.word_list_library] self.library_model.wrap(source) # __on_library_selection_changed() (invoked by _set_selected_word_list) # clears self.selected_words selected_words = self.selected_words self._set_selected_word_list(self.word_list_index) if self.words is not None: self.words_model.wrap(list(self.words)) self._set_word_list_modified(mod_type=self.CACHED) if selected_words: self.selected_words = selected_words self.set_selected_words() elif len(self.word_list_library) > self.word_list_index and \ self.word_list_library[self.word_list_index] != self.words: self.commit() def _save_state(self): self.word_list_library = [s.as_dict() for s in self.library_model] self.words = self.words_model[:] def send_report(self): library = self.library_model[self.word_list_index].name \ if self.library_model else "/" settings = [("Library", library)] if self.__input_words: self.report_data("Input Words", self.__input_words) settings.append(("Word variable", self.words_var)) rule = UpdateRules.ITEMS[self.update_rule_index] settings.append(("Update", rule)) self.report_items("Settings", settings) self.report_paragraph("Words", ", ".join(self.words_model[:]))