source: orange/Orange/OrangeWidgets/Data/OWPythonScript.py @ 11012:19029caa4a32

Revision 11012:19029caa4a32, 22.3 KB checked in by Miha Stajdohar <miha.stajdohar@…>, 18 months ago (diff)

Fixed more network references.

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