Пример #1
0
    def test_tool_grid(self):
        w = ToolGrid()

        w.show()
        self.app.processEvents()

        def buttonsOrderedVisual():
            # Process layout events so the buttons have right positions
            self.app.processEvents()
            buttons = w.findChildren(QToolButton)
            return list(sorted(buttons, key=lambda b: (b.y(), b.x())))

        def buttonsOrderedLogical():
            return list(map(w.buttonForAction, w.actions()))

        def assertOrdered():
            self.assertSequenceEqual(buttonsOrderedLogical(),
                                     buttonsOrderedVisual())

        action_a = QAction("A", w)
        action_b = QAction("B", w)
        action_c = QAction("C", w)
        action_d = QAction("D", w)

        w.addAction(action_b)
        w.insertAction(0, action_a)
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b])
        assertOrdered()

        w.addAction(action_d)
        w.insertAction(action_d, action_c)

        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_c, action_d])
        assertOrdered()

        w.removeAction(action_c)
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_d])

        assertOrdered()

        w.removeAction(action_a)
        self.assertSequenceEqual(w.actions(),
                                 [action_b, action_d])

        assertOrdered()

        w.insertAction(0, action_a)
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_d])

        assertOrdered()

        w.setColumnCount(2)
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_d])

        assertOrdered()

        w.insertAction(2, action_c)
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_c, action_d])
        assertOrdered()

        w.clear()
        # test no 'before' action edge case
        w.insertAction(0, action_a)
        self.assertIs(action_a, w.actions()[0])
        w.insertAction(1, action_b)
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b])

        w.clear()
        w.setActions([action_a, action_b, action_c, action_d])
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_c, action_d])
        assertOrdered()

        triggered_actions = []

        def p(action):
            print(action.text())

        w.actionTriggered.connect(p)
        w.actionTriggered.connect(triggered_actions.append)
        action_a.trigger()

        w.show()
        self.app.exec_()
    def test_tool_grid(self):
        w = ToolGrid()

        w.show()
        self.app.processEvents()

        def buttonsOrderedVisual():
            # Process layout events so the buttons have right positions
            self.app.processEvents()
            buttons = w.findChildren(QToolButton)
            return sorted(buttons, key=lambda b: (b.y(), b.x()))

        def buttonsOrderedLogical():
            return list(map(w.buttonForAction, w.actions()))

        def assertOrdered():
            self.assertSequenceEqual(buttonsOrderedLogical(),
                                     buttonsOrderedVisual())

        action_a = QAction("A", w)
        action_b = QAction("B", w)
        action_c = QAction("C", w)
        action_d = QAction("D", w)

        w.addAction(action_b)
        w.insertAction(0, action_a)
        self.assertSequenceEqual(w.actions(), [action_a, action_b])
        assertOrdered()

        w.addAction(action_d)
        w.insertAction(action_d, action_c)

        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_c, action_d])
        assertOrdered()

        w.removeAction(action_c)
        self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d])

        assertOrdered()

        w.removeAction(action_a)
        self.assertSequenceEqual(w.actions(), [action_b, action_d])

        assertOrdered()

        w.insertAction(0, action_a)
        self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d])

        assertOrdered()

        w.setColumnCount(2)
        self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d])

        assertOrdered()

        w.insertAction(2, action_c)
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_c, action_d])
        assertOrdered()

        w.clear()
        # test no 'before' action edge case
        w.insertAction(0, action_a)
        self.assertIs(action_a, w.actions()[0])
        w.insertAction(1, action_b)
        self.assertSequenceEqual(w.actions(), [action_a, action_b])

        w.clear()
        w.setActions([action_a, action_b, action_c, action_d])
        self.assertSequenceEqual(w.actions(),
                                 [action_a, action_b, action_c, action_d])
        assertOrdered()

        triggered_actions = []

        def p(action):
            print(action.text())

        w.actionTriggered.connect(p)
        w.actionTriggered.connect(triggered_actions.append)
        action_a.trigger()

        w.show()
        self.app.exec_()
Пример #3
0
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)

    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")

    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.setTabStopWidth(4)

        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()