source: orange/Orange/OrangeWidgets/Data/OWPythonScript.py @ 11364:f25b850d417f

Revision 11364:f25b850d417f, 23.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Added support for pasting code into the console widget.

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