source: orange/Orange/OrangeWidgets/Data/OWPythonScript.py @ 11788:1ead61cef00e

Revision 11788:1ead61cef00e, 22.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Code style fixes, removed unused functions, ...

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