source: orange/Orange/OrangeWidgets/Data/OWPythonScript.py @ 11434:e3ed3a611526

Revision 11434:e3ed3a611526, 23.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Added 'Default' flag to "in_data", "in_learner", ... channels.

They are preferred to "in_object".

Line 
1"""
2<name>Python Script</name>
3<description>Executes python script.</description>
4<icon>icons/PythonScript.svg</icon>
5<contact>Miha Stajdohar (miha.stajdohar(@at@)gmail.com)</contact>
6<priority>3150</priority>
7"""
8import sys
9import os
10import code
11import keyword
12import itertools
13
14from PyQt4.QtGui import (
15    QSyntaxHighlighter, QPlainTextEdit, QTextCharFormat, QTextCursor,
16    QTextDocument, QPlainTextDocumentLayout, QBrush, QFont, QColor, QPalette,
17    QStyledItemDelegate, QStyleOptionViewItemV4, QLineEdit, QListView,
18    QSizePolicy, QAction, QMenu, QKeySequence, QSplitter, QToolButton,
19    QItemSelectionModel, QFileDialog
20)
21
22from PyQt4.QtCore import Qt, QRegExp, QByteArray
23from PyQt4.QtCore import SIGNAL
24
25from OWWidget import *
26
27from OWItemModels import PyListModel, ModelActionsWidget
28
29import OWGUI
30import Orange
31
32
33class PythonSyntaxHighlighter(QSyntaxHighlighter):
34    def __init__(self, parent=None):
35        self.keywordFormat = QTextCharFormat()
36        self.keywordFormat.setForeground(QBrush(Qt.blue))
37        self.keywordFormat.setFontWeight(QFont.Bold)
38        self.stringFormat = QTextCharFormat()
39        self.stringFormat.setForeground(QBrush(Qt.darkGreen))
40        self.defFormat = QTextCharFormat()
41        self.defFormat.setForeground(QBrush(Qt.black))
42        self.defFormat.setFontWeight(QFont.Bold)
43        self.commentFormat = QTextCharFormat()
44        self.commentFormat.setForeground(QBrush(Qt.lightGray))
45        self.decoratorFormat = QTextCharFormat()
46        self.decoratorFormat.setForeground(QBrush(Qt.darkGray))
47
48        self.keywords = list(keyword.kwlist)
49
50        self.rules = [(QRegExp(r"\b%s\b" % kwd), self.keywordFormat)
51                      for kwd in self.keywords] + \
52                     [(QRegExp(r"\bdef\s+([A-Za-z_]+[A-Za-z0-9_]+)\s*\("),
53                       self.defFormat),
54                      (QRegExp(r"\bclass\s+([A-Za-z_]+[A-Za-z0-9_]+)\s*\("),
55                       self.defFormat),
56                      (QRegExp(r"'.*'"), self.stringFormat),
57                      (QRegExp(r'".*"'), self.stringFormat),
58                      (QRegExp(r"#.*"), self.commentFormat),
59                      (QRegExp(r"@[A-Za-z_]+[A-Za-z0-9_]+"),
60                       self.decoratorFormat)]
61
62        self.multilineStart = QRegExp(r"(''')|" + r'(""")')
63        self.multilineEnd = QRegExp(r"(''')|" + r'(""")')
64
65        QSyntaxHighlighter.__init__(self, parent)
66
67    def highlightBlock(self, text):
68        for pattern, format in self.rules:
69            exp = QRegExp(pattern)
70            index = exp.indexIn(text)
71            while index >= 0:
72                length = exp.matchedLength()
73                if exp.numCaptures() > 0:
74                    self.setFormat(exp.pos(1), len(str(exp.cap(1))), format)
75                else:
76                    self.setFormat(exp.pos(0), len(str(exp.cap(0))), format)
77                index = exp.indexIn(text, index + length)
78
79        # Multi line strings
80        start = self.multilineStart
81        end = self.multilineEnd
82
83        self.setCurrentBlockState(0)
84        startIndex, skip = 0, 0
85        if self.previousBlockState() != 1:
86            startIndex, skip = start.indexIn(text), 3
87        while startIndex >= 0:
88            endIndex = end.indexIn(text, startIndex + skip)
89            if endIndex == -1:
90                self.setCurrentBlockState(1)
91                commentLen = len(text) - startIndex
92            else:
93                commentLen = endIndex - startIndex + 3
94            self.setFormat(startIndex, commentLen, self.stringFormat)
95            startIndex, skip = (start.indexIn(text,
96                                              startIndex + commentLen + 3),
97                                3)
98
99
100class PythonScriptEditor(QPlainTextEdit):
101    INDENT = 4
102
103    def lastLine(self):
104        text = str(self.toPlainText())
105        pos = self.textCursor().position()
106        index = text.rfind("\n", 0, pos)
107        text = text[index: pos].lstrip("\n")
108        return text
109
110    def keyPressEvent(self, event):
111        if event.key() == Qt.Key_Return:
112            text = self.lastLine()
113            indent = len(text) - len(text.lstrip())
114            if text.strip() == "pass" or text.strip().startswith("return "):
115                indent = max(0, indent - self.INDENT)
116            elif text.strip().endswith(":"):
117                indent += self.INDENT
118            QPlainTextEdit.keyPressEvent(self, event)
119            self.insertPlainText(" " * indent)
120        elif event.key() == Qt.Key_Tab:
121            self.insertPlainText(" " * self.INDENT)
122        elif event.key() == Qt.Key_Backspace:
123            text = self.lastLine()
124            if text and not text.strip():
125                cursor = self.textCursor()
126                for i in range(min(self.INDENT, len(text))):
127                    cursor.deletePreviousChar()
128            else:
129                QPlainTextEdit.keyPressEvent(self, event)
130
131        else:
132            QPlainTextEdit.keyPressEvent(self, event)
133
134
135class PythonConsole(QPlainTextEdit, code.InteractiveConsole):
136    def __init__(self, locals=None, parent=None):
137        QPlainTextEdit.__init__(self, parent)
138        code.InteractiveConsole.__init__(self, locals)
139        self.history, self.historyInd = [""], 0
140        self.loop = self.interact()
141        self.loop.next()
142
143    def setLocals(self, locals):
144        self.locals = locals
145
146    def interact(self, banner=None):
147        try:
148            sys.ps1
149        except AttributeError:
150            sys.ps1 = ">>> "
151        try:
152            sys.ps2
153        except AttributeError:
154            sys.ps2 = "... "
155        cprt = ('Type "help", "copyright", "credits" or "license" '
156                'for more information.')
157        if banner is None:
158            self.write("Python %s on %s\n%s\n(%s)\n" %
159                       (sys.version, sys.platform, cprt,
160                        self.__class__.__name__))
161        else:
162            self.write("%s\n" % str(banner))
163        more = 0
164        while 1:
165            try:
166                if more:
167                    prompt = sys.ps2
168                else:
169                    prompt = sys.ps1
170                self.new_prompt(prompt)
171                yield
172                try:
173                    line = self.raw_input(prompt)
174                except EOFError:
175                    self.write("\n")
176                    break
177                else:
178                    more = self.push(line)
179            except KeyboardInterrupt:
180                self.write("\nKeyboardInterrupt\n")
181                self.resetbuffer()
182                more = 0
183
184    def raw_input(self, prompt):
185        input = str(self.document().lastBlock().previous().text())
186        return input[len(prompt):]
187
188    def new_prompt(self, prompt):
189        self.write(prompt)
190        self.newPromptPos = self.textCursor().position()
191
192    def write(self, data):
193        cursor = QTextCursor(self.document())
194        cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor)
195        cursor.insertText(data)
196        self.setTextCursor(cursor)
197        self.ensureCursorVisible()
198
199    def push(self, line):
200        if self.history[0] != line:
201            self.history.insert(0, line)
202        self.historyInd = 0
203
204        saved = sys.stdout, sys.stderr
205        try:
206            sys.stdout, sys.stderr = self, self
207            return code.InteractiveConsole.push(self, line)
208        finally:
209            sys.stdout, sys.stderr = saved
210
211    def setLine(self, line):
212        cursor = QTextCursor(self.document())
213        cursor.movePosition(QTextCursor.End)
214        cursor.setPosition(self.newPromptPos, QTextCursor.KeepAnchor)
215        cursor.removeSelectedText()
216        cursor.insertText(line)
217        self.setTextCursor(cursor)
218
219    def keyPressEvent(self, event):
220        if event.key() == Qt.Key_Return:
221            self.write("\n")
222            self.loop.next()
223        elif event.key() == Qt.Key_Up:
224            self.historyUp()
225        elif event.key() == Qt.Key_Down:
226            self.historyDown()
227        elif event.key() == Qt.Key_Tab:
228            self.complete()
229        elif event.key() in [Qt.Key_Left, Qt.Key_Backspace]:
230            if self.textCursor().position() > self.newPromptPos:
231                QPlainTextEdit.keyPressEvent(self, event)
232        else:
233            QPlainTextEdit.keyPressEvent(self, event)
234
235    def historyUp(self):
236        self.setLine(self.history[self.historyInd])
237        self.historyInd = min(self.historyInd + 1, len(self.history) - 1)
238
239    def historyDown(self):
240        self.setLine(self.history[self.historyInd])
241        self.historyInd = max(self.historyInd - 1, 0)
242
243    def complete(self):
244        pass
245
246    def _moveCursorToInputLine(self):
247        """
248        Move the cursor to the input line if not already there. If the cursor
249        if already in the input line (at position greater or equal to
250        `newPromptPos`) it is left unchanged, otherwise it is moved at the
251        end.
252
253        """
254        cursor = self.textCursor()
255        pos = cursor.position()
256        if pos < self.newPromptPos:
257            cursor.movePosition(QTextCursor.End)
258            self.setTextCursor(cursor)
259
260    def pasteCode(self, source):
261        """
262        Paste source code into the console.
263        """
264        self._moveCursorToInputLine()
265
266        for line in interleave(source.splitlines(), itertools.repeat("\n")):
267            if line != "\n":
268                self.insertPlainText(line)
269            else:
270                self.write("\n")
271                self.loop.next()
272
273    def insertFromMimeData(self, source):
274        """
275        Reimplemented from QPlainTextEdit.insertFromMimeData.
276        """
277        if source.hasText():
278            self.pasteCode(unicode(source.text()))
279            return
280
281
282def interleave(seq1, seq2):
283    """
284    Interleave elements of `seq2` between consecutive elements of `seq1`.
285
286        >>> list(interleave([1, 3, 5], [2, 4]))
287        [1, 2, 3, 4, 5]
288
289    """
290    iterator1, iterator2 = iter(seq1), iter(seq2)
291    leading = next(iterator1)
292    for element in iterator1:
293        yield leading
294        yield next(iterator2)
295        leading = element
296
297    yield leading
298
299
300class Script(object):
301    Modified = 1
302    MissingFromFilesystem = 2
303
304    def __init__(self, name, script, flags=0, sourceFileName=None):
305        self.name = name
306        self.script = script
307        self.flags = flags
308        self.sourceFileName = sourceFileName
309        self.modifiedScript = None
310
311
312class ScriptItemDelegate(QStyledItemDelegate):
313    def __init__(self, parent):
314        QStyledItemDelegate.__init__(self, parent)
315
316    def displayText(self, variant, locale):
317        script = variant.toPyObject()
318        if script.flags & Script.Modified:
319            return QString("*" + script.name)
320        else:
321            return QString(script.name)
322
323    def paint(self, painter, option, index):
324        script = index.data(Qt.DisplayRole).toPyObject()
325
326        if script.flags & Script.Modified:
327            option = QStyleOptionViewItemV4(option)
328            option.palette.setColor(QPalette.Text, QColor(Qt.red))
329            option.palette.setColor(QPalette.Highlight, QColor(Qt.darkRed))
330        QStyledItemDelegate.paint(self, painter, option, index)
331
332    def createEditor(self, parent, option, index):
333        return QLineEdit(parent)
334
335    def setEditorData(self, editor, index):
336        script = index.data(Qt.DisplayRole).toPyObject()
337        editor.setText(script.name)
338
339    def setModelData(self, editor, model, index):
340        model[index.row()].name = str(editor.text())
341
342
343class OWPythonScript(OWWidget):
344
345    settingsList = ["codeFile", "libraryListSource", "currentScriptIndex",
346                    "splitterState", "auto_execute"]
347
348    def __init__(self, parent=None, signalManager=None):
349        OWWidget.__init__(self, parent, signalManager, 'Python Script')
350
351        self.inputs = [("in_data", Orange.data.Table, self.setExampleTable,
352                        Default),
353                       ("in_distance", Orange.misc.SymMatrix,
354                        self.setDistanceMatrix, Default),
355                       ("in_learner", Orange.core.Learner, self.setLearner,
356                        Default),
357                       ("in_classifier", Orange.core.Classifier,
358                        self.setClassifier, Default),
359                       ("in_object", object, self.setObject)]
360
361        self.outputs = [("out_data", Orange.data.Table),
362                        ("out_distance", Orange.misc.SymMatrix),
363                        ("out_learner", Orange.core.Learner),
364                        ("out_classifier", Orange.core.Classifier, Dynamic),
365                        ("out_object", object, Dynamic)]
366
367        self.in_data = None
368        self.in_distance = None
369        self.in_learner = None
370        self.in_classifier = None
371        self.in_object = None
372        self.auto_execute = False
373
374        self.codeFile = ''
375        self.libraryListSource = [Script("Hello world",
376                                         "print 'Hello world'\n")]
377        self.currentScriptIndex = 0
378        self.splitterState = None
379        self.loadSettings()
380
381        for s in self.libraryListSource:
382            s.flags = 0
383
384        self._cachedDocuments = {}
385
386        self.infoBox = OWGUI.widgetBox(self.controlArea, 'Info')
387        OWGUI.label(
388            self.infoBox, self,
389            "<p>Execute python script.</p><p>Input variables:<ul><li> " + \
390            "<li>".join(t[0] for t in self.inputs) + \
391            "</ul></p><p>Output variables:<ul><li>" + \
392            "<li>".join(t[0] for t in self.outputs) + \
393            "</ul></p>"
394        )
395
396        self.libraryList = PyListModel([], self, flags=Qt.ItemIsSelectable | \
397                                       Qt.ItemIsEnabled | Qt.ItemIsEditable)
398
399        self.libraryList.wrap(self.libraryListSource)
400
401        self.controlBox = OWGUI.widgetBox(self.controlArea, 'Library')
402        self.controlBox.layout().setSpacing(1)
403
404        self.libraryView = QListView()
405        self.libraryView.setEditTriggers(QListView.DoubleClicked | \
406                                         QListView.EditKeyPressed)
407        self.libraryView.setSizePolicy(QSizePolicy.Ignored,
408                                       QSizePolicy.Preferred)
409        self.libraryView.setItemDelegate(ScriptItemDelegate(self))
410        self.libraryView.setModel(self.libraryList)
411
412        self.connect(self.libraryView.selectionModel(),
413                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
414                     self.onSelectedScriptChanged)
415        self.controlBox.layout().addWidget(self.libraryView)
416
417        w = ModelActionsWidget()
418
419        self.addNewScriptAction = action = QAction("+", self)
420        action.setToolTip("Add a new script to the library")
421        self.connect(action, SIGNAL("triggered()"), self.onAddScript)
422        w.addAction(action)
423
424        self.removeAction = action = QAction("-", self)
425        action.setToolTip("Remove script from library")
426        self.connect(action, SIGNAL("triggered()"), self.onRemoveScript)
427        w.addAction(action)
428
429        action = QAction("Update", self)
430        action.setToolTip("Save changes in the editor to library")
431        action.setShortcut(QKeySequence(QKeySequence.Save))
432        self.connect(action, SIGNAL("triggered()"),
433                     self.commitChangesToLibrary)
434        w.addAction(action)
435
436        action = QAction("More", self)
437        action.pyqtConfigure(toolTip="More actions")
438
439        new_from_file = QAction("Import a script from a file", self)
440        save_to_file = QAction("Save selected script to a file", self)
441        save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs))
442
443        self.connect(new_from_file, SIGNAL("triggered()"),
444                     self.onAddScriptFromFile)
445        self.connect(save_to_file, SIGNAL("triggered()"), self.saveScript)
446
447        menu = QMenu(w)
448        menu.addAction(new_from_file)
449        menu.addAction(save_to_file)
450        action.setMenu(menu)
451        button = w.addAction(action)
452        button.setPopupMode(QToolButton.InstantPopup)
453
454        w.layout().setSpacing(1)
455
456        self.controlBox.layout().addWidget(w)
457
458        self.runBox = OWGUI.widgetBox(self.controlArea, 'Run')
459        OWGUI.button(self.runBox, self, "Execute", callback=self.execute)
460        OWGUI.checkBox(self.runBox, self, "auto_execute", "Auto execute",
461                       tooltip=("Run the script automatically whenever "
462                                "the inputs to the widget change."))
463
464        self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea)
465        self.mainArea.layout().addWidget(self.splitCanvas)
466
467        self.defaultFont = defaultFont = \
468            "Monaco" if sys.platform == "darwin" else "Courier"
469
470        self.textBox = OWGUI.widgetBox(self, 'Python script')
471        self.splitCanvas.addWidget(self.textBox)
472        self.text = PythonScriptEditor(self)
473        self.textBox.layout().addWidget(self.text)
474
475        self.textBox.setAlignment(Qt.AlignVCenter)
476        self.text.setTabStopWidth(4)
477
478        self.connect(self.text, SIGNAL("modificationChanged(bool)"),
479                     self.onModificationChanged)
480
481        self.saveAction = action = QAction("&Save", self.text)
482        action.setToolTip("Save script to file")
483        action.setShortcut(QKeySequence(QKeySequence.Save))
484        action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
485        self.connect(action, SIGNAL("triggered()"), self.saveScript)
486
487        self.consoleBox = OWGUI.widgetBox(self, 'Console')
488        self.splitCanvas.addWidget(self.consoleBox)
489        self.console = PythonConsole(self.__dict__, self)
490        self.consoleBox.layout().addWidget(self.console)
491        self.console.document().setDefaultFont(QFont(defaultFont))
492        self.consoleBox.setAlignment(Qt.AlignBottom)
493        self.console.setTabStopWidth(4)
494
495        self.openScript(self.codeFile)
496        try:
497            self.libraryView.selectionModel().select(
498                self.libraryList.index(self.currentScriptIndex),
499                QItemSelectionModel.ClearAndSelect
500            )
501        except Exception:
502            pass
503
504        self.splitCanvas.setSizes([2, 1])
505        if self.splitterState is not None:
506            self.splitCanvas.restoreState(QByteArray(self.splitterState))
507
508        self.connect(self.splitCanvas, SIGNAL("splitterMoved(int, int)"),
509                     self.onSpliterMoved)
510        self.controlArea.layout().addStretch(1)
511        self.resize(800, 600)
512
513    def setExampleTable(self, et):
514        self.in_data = et
515
516    def setDistanceMatrix(self, dm):
517        self.in_distance = dm
518
519    def setLearner(self, learner):
520        self.in_learner = learner
521
522    def setClassifier(self, classifier):
523        self.in_classifier = classifier
524
525    def setObject(self, obj):
526        self.in_object = obj
527
528    def handleNewSignals(self):
529        if self.auto_execute:
530            self.execute()
531
532    def selectedScriptIndex(self):
533        rows = self.libraryView.selectionModel().selectedRows()
534        if rows:
535            return  [i.row() for i in rows][0]
536        else:
537            return None
538
539    def setSelectedScript(self, index):
540        selection = self.libraryView.selectionModel()
541        selection.select(self.libraryList.index(index),
542                         QItemSelectionModel.ClearAndSelect)
543
544    def onAddScript(self, *args):
545        self.libraryList.append(Script("New script", "", 0))
546        self.setSelectedScript(len(self.libraryList) - 1)
547
548    def onAddScriptFromFile(self, *args):
549        filename = QFileDialog.getOpenFileName(
550            self, 'Open Python Script',
551            self.codeFile,
552            'Python files (*.py)\nAll files(*.*)'
553        )
554
555        filename = unicode(filename)
556        if filename:
557            name = os.path.basename(filename)
558            self.libraryList.append(Script(name, open(filename, "rb").read(),
559                                           0, filename))
560            self.setSelectedScript(len(self.libraryList) - 1)
561
562    def onRemoveScript(self, *args):
563        index = self.selectedScriptIndex()
564        if index is not None:
565            del self.libraryList[index]
566
567            self.libraryView.selectionModel().select(
568                self.libraryList.index(max(index - 1, 0)),
569                QItemSelectionModel.ClearAndSelect
570            )
571
572    def onSaveScriptToFile(self, *args):
573        index = self.selectedScriptIndex()
574        if index is not None:
575            self.saveScript()
576
577    def onSelectedScriptChanged(self, selected, deselected):
578        index = [i.row() for i in selected.indexes()]
579        if index:
580            current = index[0]
581            if current >= len(self.libraryList):
582                self.addNewScriptAction.trigger()
583                return
584
585            self.text.setDocument(self.documentForScript(current))
586            self.currentScriptIndex = current
587
588    def documentForScript(self, script=0):
589        if type(script) != Script:
590            script = self.libraryList[script]
591
592        if script not in self._cachedDocuments:
593            doc = QTextDocument(self)
594            doc.setDocumentLayout(QPlainTextDocumentLayout(doc))
595            doc.setPlainText(script.script)
596            doc.setDefaultFont(QFont(self.defaultFont))
597            doc.highlighter = PythonSyntaxHighlighter(doc)
598            self.connect(doc, SIGNAL("modificationChanged(bool)"),
599                         self.onModificationChanged)
600            doc.setModified(False)
601            self._cachedDocuments[script] = doc
602        return self._cachedDocuments[script]
603
604    def commitChangesToLibrary(self, *args):
605        index = self.selectedScriptIndex()
606        if index is not None:
607            self.libraryList[index].script = self.text.toPlainText()
608            self.text.document().setModified(False)
609            self.libraryList.emitDataChanged(index)
610
611    def onModificationChanged(self, modified):
612        index = self.selectedScriptIndex()
613        if index is not None:
614            self.libraryList[index].flags = Script.Modified if modified else 0
615            self.libraryList.emitDataChanged(index)
616
617    def onSpliterMoved(self, pos, ind):
618        self.splitterState = str(self.splitCanvas.saveState())
619
620    def updateSelecetdScriptState(self):
621        index = self.selectedScriptIndex()
622        if index is not None:
623            script = self.libraryList[index]
624            self.libraryList[index] = Script(script.name,
625                                             self.text.toPlainText(),
626                                             0)
627
628    def openScript(self, filename=None):
629        if filename == None:
630            filename = QFileDialog.getOpenFileName(
631                self, 'Open Python Script',
632                self.codeFile,
633                'Python files (*.py)\nAll files(*.*)'
634            )
635
636            filename = unicode(filename)
637            self.codeFile = filename
638        else:
639            self.codeFile = filename
640
641        if self.codeFile == "":
642            return
643
644        self.error(0)
645        try:
646            f = open(self.codeFile, 'r')
647        except (IOError, OSError), ex:
648            self.text.setPlainText("")
649            return
650
651        self.text.setPlainText(f.read())
652        f.close()
653
654    def saveScript(self):
655        index = self.selectedScriptIndex()
656        if index is not None:
657            script = self.libraryList[index]
658            filename = script.sourceFileName or self.codeFile
659        else:
660            filename = self.codeFile
661        filename = QFileDialog.getSaveFileName(
662            self, 'Save Python Script',
663            filename,
664            'Python files (*.py)\nAll files(*.*)'
665        )
666
667        self.codeFile = unicode(filename)
668
669        if self.codeFile:
670            fn = ""
671            head, tail = os.path.splitext(self.codeFile)
672            if not tail:
673                fn = head + ".py"
674            else:
675                fn = self.codeFile
676
677            f = open(fn, 'w')
678            f.write(self.text.toPlainText())
679            f.close()
680
681    def execute(self):
682        self._script = str(self.text.toPlainText())
683        self.console.write("\nRunning script:\n")
684        self.console.push("exec(_script)")
685        self.console.new_prompt(sys.ps1)
686        for out in self.outputs:
687            signal = out[0]
688            self.send(signal, getattr(self, signal, None))
689
690
691if __name__ == "__main__":
692    appl = QApplication(sys.argv)
693    ow = OWPythonScript()
694    ow.show()
695    appl.exec_()
696    ow.saveSettings()
Note: See TracBrowser for help on using the repository browser.