source: orange/orange/OrangeWidgets/Data/OWPythonScript.py @ 9565:d632308bdf9a

Revision 9565:d632308bdf9a, 22.1 KB checked in by ales_erjavec <ales.erjavec@…>, 2 years ago (diff)

orngNetwork.Network -> Orange.network.Graph

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