source: orange/Orange/OrangeWidgets/Data/OWPythonScript.py @ 11329:0873f756fe02

Revision 11329:0873f756fe02, 22.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Added 'Auto execute' option.

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