source: orange/orange/OrangeWidgets/Data/OWPythonScript.py @ 9044:6154f6ee9601

Revision 9044:6154f6ee9601, 22.1 KB checked in by mitar, 3 years ago (diff)

Moving widget icons around and renaming.

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