source: orange/orange/OrangeWidgets/Prototypes/OWRScript.py @ 9123:57f5fd2c13eb

Revision 9123:57f5fd2c13eb, 15.3 KB checked in by ales_erjavec <ales.erjavec@…>, 3 years ago (diff)

Using QFileSystemWatcher for watch for changes in the device file.
Fixed R console error handling.

Line 
1"""<name>R Script</name>
2<description>Run R scripts</description>
3<icon>icons/RScript.png</icon>
4"""
5
6
7from OWWidget import *
8from OWPythonScript import OWPythonScript, Script, ScriptItemDelegate, PythonConsole
9from OWItemModels import PyListModel, ModelActionsWidget
10
11import Orange.misc.r as r
12
13globalenv = r.globalenv
14
15import rpy2.rinterface
16import rpy2.robjects as robjects
17import rpy2.rlike.container as rlc
18
19class RPy2Console(PythonConsole):
20   
21    def interact(self, banner=None):
22        if banner is None:
23            banner ="R console:"
24        return PythonConsole.interact(self, banner)
25       
26    def push(self, line):
27        if self.history[0] != line:
28            self.history.insert(0, line)
29        self.historyInd = 0
30        more = 0
31        try:
32            r_res = self._rpy(line)
33            if not isinstance(r_res, rpy2.rinterface.RNULLType):
34                self.write(r_res.r_repr())
35        except rpy2.rinterface.RRuntimeError, ex:
36            self.write(ex.message)
37        except Exception, ex:
38            self.write(repr(ex))
39       
40        self.write("\n")
41       
42    def _rpy(self, script):
43        r_res = robjects.r(script)
44        return r_res
45   
46    def setLocals(self, locals):
47        r_obj = {}
48        for key, val in locals.items():
49            if isinstance(val, orange.ExampleTable):
50                dataframe = r.dataframe(val)
51                globalenv[key] = dataframe
52            elif isinstance(val, orange.SymMatrix):
53                matrix = r.matrix(val)
54                globalenv[key] = matrix
55               
56               
57class RSyntaxHighlighter(QSyntaxHighlighter):
58    def __init__(self, parent=None):
59        self.keywordFormat = QTextCharFormat()
60        self.keywordFormat.setForeground(QBrush(Qt.darkGray))
61        self.keywordFormat.setFontWeight(QFont.Bold)
62        self.stringFormat = QTextCharFormat()
63        self.stringFormat.setForeground(QBrush(Qt.darkGreen))
64        self.defFormat = QTextCharFormat()
65        self.defFormat.setForeground(QBrush(Qt.black))
66        self.defFormat.setFontWeight(QFont.Bold)
67        self.commentFormat = QTextCharFormat()
68        self.commentFormat.setForeground(QBrush(Qt.lightGray))
69        self.decoratorFormat = QTextCharFormat()
70        self.decoratorFormat.setForeground(QBrush(Qt.darkGray))
71        self.constantFormat = QTextCharFormat()
72        self.constantFormat.setForeground(QBrush(Qt.blue))
73       
74        self.keywords = ["TRUE", "FALSE", "if", "else", "NULL", r"\.\.\.", "<-", "for", "while", "repeat", "next",
75                         "break", "switch", "function"]
76        self.rules = [(QRegExp(r"\b%s\b" % keyword), self.keywordFormat) for keyword in self.keywords] + \
77                     [
78#                     [(QRegExp(r"\bdef\s+([A-Za-z_]+[A-Za-z0-9_]+)\s*\("), self.defFormat),
79#                      (QRegExp(r"\bclass\s+([A-Za-z_]+[A-Za-z0-9_]+)\s*\("), self.defFormat),
80                      (QRegExp(r"'.*'"), self.stringFormat),
81                      (QRegExp(r'".*"'), self.stringFormat),
82                      (QRegExp(r"#.*"), self.commentFormat),
83                      (QRegExp(r"[0-9]+\.?[0-9]*"), self.constantFormat)
84#                      (QRegExp(r"@[A-Za-z_]+[A-Za-z0-9_]+"), self.decoratorFormat)]
85                     ]
86                     
87        QSyntaxHighlighter.__init__(self, parent)
88       
89    def highlightBlock(self, text):
90        for pattern, format in self.rules:
91            exp = QRegExp(pattern)
92            index = exp.indexIn(text)
93            while index >= 0:
94                length = exp.matchedLength()
95                if exp.numCaptures() > 0:
96                    self.setFormat(exp.pos(1), len(str(exp.cap(1))), format)
97                else:
98                    self.setFormat(exp.pos(0), len(str(exp.cap(0))), format)
99                index = exp.indexIn(text, index + length)
100               
101        ## Multi line strings
102#        start = QRegExp(r"(''')|" + r'(""")')
103#        end = QRegExp(r"(''')|" + r'(""")')
104#        self.setCurrentBlockState(0)
105#        startIndex, skip = 0, 0
106#        if self.previousBlockState() != 1:
107#            startIndex, skip = start.indexIn(text), 3
108#        while startIndex >= 0:
109#            endIndex = end.indexIn(text, startIndex + skip)
110#            if endIndex == -1:
111#                self.setCurrentBlockState(1)
112#                commentLen = len(text) - startIndex
113#            else:
114#                commentLen = endIndex - startIndex + 3
115#            self.setFormat(startIndex, commentLen, self.stringFormat)
116#            startIndex, skip = start.indexIn(text, startIndex + commentLen + 3), 3
117
118class RScriptEditor(QPlainTextEdit):
119    pass
120
121class RScript(Script):
122    pass
123
124class LibraryWidget(QWidget):
125    def __init__(self, parent=None):
126        QWidget.__init__(self, parent)
127        self.setLayout(QVBoxLayout())
128       
129        self.layout().setSpacing(1)
130        self.layout().setContentsMargins(0, 0, 0, 0)
131        self.view = QListView(self)
132        self.view.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)
133        self.layout().addWidget(self.view)
134       
135        self.addScriptAction = QAction("+", self)
136        self.addScriptAction.pyqtConfigure(toolTip="Add a new script to library")
137       
138        self.updateScriptAction = QAction("Update", self)
139        self.updateScriptAction.pyqtConfigure(toolTip="Save changes in the editor to library", shortcut=QKeySequence.Save)
140       
141        self.removeScriptAction = QAction("-", self)
142        self.removeScriptAction.pyqtConfigure(toolTip="Remove selected script from file")
143       
144        self.moreAction = QAction("More", self)
145        self.moreAction.pyqtConfigure(toolTip="More actions")#, icon=self.style().standardIcon(QStyle.SP_ToolBarHorizontalExtensionButton))
146       
147        self.addScriptFromFile = QAction("Add script from file", self)
148        self.addScriptFromFile.pyqtConfigure(toolTip="Add a new script to library from a file")
149       
150        self.saveScriptToFile = QAction("Save script to file", self)
151        self.saveScriptToFile.pyqtConfigure(toolTip="Save script to a file", shortcut=QKeySequence.SaveAs)
152       
153        menu = QMenu(self)
154        menu.addActions([self.addScriptFromFile, self.saveScriptToFile])
155        self.moreAction.setMenu(menu)
156       
157        self.actionsWidget = ModelActionsWidget([self.addScriptAction, self.updateScriptAction, 
158                                            self.removeScriptAction, self.moreAction], self)
159#        self.actionsWidget.buttons[1].setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
160        self.actionsWidget.layout().setSpacing(1)
161       
162        self.layout().addWidget(self.actionsWidget)
163       
164        self.resize(800, 600)
165       
166       
167    def setScriptModel(self, model):
168        """ Set the script model to show
169        """
170        self.model = model
171        if self.view is not None:
172            self.view.setModel(model)
173       
174       
175    def setView(self, view):
176        """ Set the view (QListView or subclass) to use
177        """
178        if self.view is not None:
179            self.layout().removeItemAt(0) 
180        self.view = view
181        self.layout().insertItem(0, view)
182        if self.model is not None:
183            self.view.setModel(self.model)
184           
185           
186    def setActionsWidget(self, widget):
187        """ Set the widget below the view to widget (removing the previous widget)
188        """
189        self.layout().removeItemAt(1)
190        self.actionsWidget = widget
191        self.layout().insertWidget(1, widget)
192
193    def setDocumentEditor(self, editor):
194        self.editor = editor
195       
196
197class OWRScript(OWWidget):
198    settingsList = ["scriptLibraryList", "selectedScriptIndex", "lastDir"]
199    def __init__(self, parent=None, signalManager=None, name="R Script"):
200        OWWidget.__init__(self, parent, signalManager, name)
201       
202        self.inputs = [("in_data", ExampleTable, self.setData), ("in_distance", orange.SymMatrix, self.setDistance)]
203        self.outputs = [("out_data", ExampleTable), ("out_distance", orange.SymMatrix), ("out_learner", orange.Learner), ("out_classifier", orange.Classifier)]
204       
205        self.in_data, self.in_distance = None, None
206        self.scriptLibraryList = [RScript("New script", "x <- c(1,2,5)\ny <- c(2,1 6)\nplot(x,y)\n")]
207        self.selectedScriptIndex = 0
208        self.selectedGrDev = "PNG"
209       
210        self.lastDir = os.path.expanduser("~/script.R")
211       
212        self.loadSettings()
213       
214        self.defaultFont = QFont("Monaco") if sys.platform == "darwin" else QFont("Courier")
215       
216        self.splitter = QSplitter(Qt.Vertical, self.mainArea)
217        self.mainArea.layout().addWidget(self.splitter)
218        self.scriptEdit = RScriptEditor()
219        self.splitter.addWidget(self.scriptEdit)
220        self.console = RPy2Console({}, self)
221        self.splitter.addWidget(self.console)
222        self.splitter.setSizes([2,1])
223       
224        self.libraryModel = PyListModel([], self, Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable)
225        self.libraryModel.wrap(self.scriptLibraryList)
226       
227        self.infoBox = OWGUI.widgetLabel(OWGUI.widgetBox(self.controlArea, "Info", addSpace=True), "")
228        self.infoBox.setText("<p>Execute R script</p>Input variables<li><ul>in_data</ul><ul>in_distance</ul></li>Output variables:<li><ul>out_data</ul></p/>")
229       
230        box = OWGUI.widgetBox(self.controlArea, "Library", addSpace=True)
231       
232        self.libraryWidget = LibraryWidget()
233        self.libraryView = self.libraryWidget.view
234        box.layout().addWidget(self.libraryWidget)
235       
236        self.libraryWidget.view.setItemDelegate(ScriptItemDelegate(self))
237        self.libraryWidget.setScriptModel(self.libraryModel)
238        self.libraryWidget.setDocumentEditor(self.scriptEdit)
239       
240        box = OWGUI.widgetBox(self.controlArea, "Run")
241        OWGUI.button(box, self, "Execute", callback=self.runScript, tooltip="Execute the script")
242       
243        OWGUI.rubber(self.controlArea)
244       
245       
246        self.connect(self.libraryWidget.addScriptAction, SIGNAL("triggered()"), self.onAddNewScript)
247        self.connect(self.libraryWidget.updateScriptAction, SIGNAL("triggered()"), self.onUpdateScript)
248        self.connect(self.libraryWidget.removeScriptAction, SIGNAL("triggered()"), self.onRemoveScript)
249        self.connect(self.libraryWidget.addScriptFromFile, SIGNAL("triggered()"), self.onAddScriptFromFile)
250        self.connect(self.libraryWidget.saveScriptToFile, SIGNAL("triggered()"), self.onSaveScriptToFile)
251       
252        self.connect(self.libraryView.selectionModel(), SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self.onScriptSelection)
253       
254        self._cachedDocuments = {}
255       
256        self.grView = QLabel()
257       
258        self.resize(800, 600)
259        QTimer.singleShot(30, self.initGrDevice)
260       
261    def initGrDevice(self):
262        import tempfile
263        self.grDevFile = tempfile.NamedTemporaryFile("rwb", prefix="orange-rscript-grDev", suffix=".png", delete=False)
264        self.grDevFile.close()
265        robjects.r('png("%s", width=512, height=512)' % self.grDevFile.name)
266        self.fileWatcher = QFileSystemWatcher(self)
267        self.fileWatcher.addPath(self.grDevFile.name)
268        self.connect(self.fileWatcher, SIGNAL("fileChanged(QString)"), self.updateGraphicsView)
269       
270    def updateGraphicsView(self, filename):
271        self.grView.setPixmap(QPixmap(filename))
272        self.grView.show()
273        if self.grDevFile.name not in list(self.fileWatcher.files()):
274            # The file can be removed and recreated by R, in which case we need to re-add it
275            self.fileWatcher.addPath(self.grDevFile.name)
276
277    def selectScript(self, index):
278        index = self.libraryModel.index(index)
279        self.libraryView.selectionModel().select(index, QItemSelectionModel.ClearAndSelect)
280       
281    def selectedScript(self):
282        rows = self.libraryView.selectionModel().selectedRows()
283        rows = [index.row() for index in rows]
284        if rows:
285            return rows[0]
286        else:
287            return None
288       
289    def onAddNewScript(self):
290        self.libraryModel.append(RScript("New Script", ""))
291        self.selectScript(len(self.libraryModel) - 1)
292       
293    def onRemoveScript(self):
294        row = self.selectedScript()
295        if row is not None:
296            del self.libraryModel[row]
297       
298    def onUpdateScript(self):
299        row = self.selectedScript()
300        if row is not None:
301            self.libraryModel[row].script = str(self.scriptEdit.toPlainText())
302            self.scriptEdit.document().setModified(False)
303            self.libraryModel.emitDataChanged([row])   
304       
305    def onAddScriptFromFile(self):
306        filename = str(QFileDialog.getOpenFileName(self, "Open script", self.lastDir))
307        if filename:
308            script = open(filename, "rb").read()
309            self.lastDir, name = os.path.split(filename)
310            self.libraryModel.append(RScript(name, script, sourceFileName=filename))
311            self.selectScript(len(self.libraryModel) - 1)   
312           
313    def onSaveScriptToFile(self):
314        row = self.selectedScript()
315        if row is not None:
316            script = self.libraryModel[row]
317            filename = str(QFileDialog.getSaveFileName(self, "Save Script As", script.sourceFileName or self.lastDir))
318            if filename:
319                self.lastDir, name = os.path.split(filename)
320                script.sourceFileName = filename
321                script.flags = 0
322                open(filename, "wb").write(script.script)
323                       
324    def onScriptSelection(self, *args):
325        row = self.selectedScript()
326        if row is not None:
327            self.scriptEdit.setDocument(self.documentForScript(row))
328                       
329    def documentForScript(self, script=0):
330        if type(script) != RScript:
331            script = self.libraryModel[script]
332        if script not in self._cachedDocuments:
333            doc = QTextDocument(self)
334            doc.setDocumentLayout(QPlainTextDocumentLayout(doc))
335            doc.setPlainText(script.script)
336            doc.setDefaultFont(QFont(self.defaultFont))
337            doc.highlighter = RSyntaxHighlighter(doc)
338            self.connect(doc, SIGNAL("modificationChanged(bool)"), self.onModificationChanged)
339            doc.setModified(False)
340            self._cachedDocuments[script] = doc
341        return self._cachedDocuments[script]
342   
343    def onModificationChanged(self, changed):
344        row = self.selectedScript()
345        if row is not None:
346            self.libraryModel[row].flags = RScript.Modified if changed else 0
347            self.libraryModel.emitDataChanged([row]) 
348                             
349    def setData(self, data):
350        self.in_data = data
351        self.console.setLocals(self.getLocals())
352
353    def setDistance(self, matrix):
354        self.in_distance = matrix
355        self.console.setLocals(self.getLocals())
356
357    def getLocals(self):
358        return {"in_data": self.in_data,
359                "in_distance": self.in_distance,
360               }
361       
362    def runScript(self):
363        self.console.push('png("%s", width=512, height=512)\n' % self.grDevFile.name)
364        self.console.push(str(self.scriptEdit.toPlainText()))
365        self.console.new_prompt(">>> ")
366        robjects.r("dev.off()\n")
367       
368   
369if __name__ == "__main__":
370    app = QApplication(sys.argv)
371    w = OWRScript()
372    data = orange.ExampleTable("../../doc/datasets/iris.tab")
373    w.setData(data)
374    w.show()
375    app.exec_()
376    w.saveSettings()
377       
378             
379       
Note: See TracBrowser for help on using the repository browser.