source: sasview/src/sas/sasgui/perspectives/calculator/pyconsole.py @ a5e1b6ca

ticket-1249
Last change on this file since a5e1b6ca was e2663b7, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

remove cached model before attempting reload. Refs #1142

  • Property mode set to 100644
File size: 12.9 KB
Line 
1"""
2Console Module display Python console
3"""
4import sys
5import os
6
7import numpy as np
8
9import wx
10from wx.lib.dialogs import ScrolledMessageDialog
11from wx.lib import layoutf
12
13import wx.py.editor as editor
14
15if sys.platform.count("win32") > 0:
16    PANEL_WIDTH = 800
17    PANEL_HEIGHT = 700
18    FONT_VARIANT = 0
19else:
20    PANEL_WIDTH = 830
21    PANEL_HEIGHT = 730
22    FONT_VARIANT = 1
23ID_CHECK_MODEL = wx.NewId()
24ID_RUN = wx.NewId()
25
26def check_model(path):
27    """
28    Check that the model on the path can run.
29    """
30    # TODO: fix model caching
31    # model_test.run_one() is directly forcing a reload of the module, but
32    # sasview_model is caching models that have already been loaded.
33    # If the sasview load happens before the test, then the module is
34    # reloaded out from under it, which causes the global variables in
35    # the model function definitions to be cleared (at least in python 2.7).
36    # To fix the proximal problem of models failing on test, perform the
37    # run_one() tests first.  To fix the deeper problem we should either
38    # remove caching from sasmodels.sasview_model.load_custom_model() or
39    # add caching to sasmodels.custom.load_custom_kernel_module().  Another
40    # option is to add a runTests method to SasviewModel which runs the
41    # test suite directly from the model info structure.  Probably some
42    # combination of options:
43    #    (1) have this function (check_model) operate on a loaded model
44    #    so that caching isn't needed in sasview_models.load_custom_model
45    #    (2) add the runTests method to SasviewModel so that tests can
46    #    be run on a loaded module.
47    #
48    # Also, note that the model test suite runs the equivalent of the
49    # "try running the model" block below, and doesn't need to be run
50    # twice.  The reason for duplicating the block here is to generate
51    # an exception that show_model_output can catch.  Need to write the
52    # runTests method so that it returns success flag as well as output
53    # string so that the extra test is not necessary.
54
55    # check the model's unit tests run
56    from sasmodels.model_test import run_one
57    result = run_one(path)
58
59    # remove cached version of the model, if any
60    from sasmodels import sasview_model
61    sasview_model.MODEL_BY_PATH.pop(path, None)
62
63    # try running the model
64    Model = sasview_model.load_custom_model(path)
65    model = Model()
66    q =  np.array([0.01, 0.1])
67    Iq = model.evalDistribution(q)
68    qx, qy =  np.array([0.01, 0.01]), np.array([0.1, 0.1])
69    Iqxy = model.evalDistribution([qx, qy])
70
71    return result
72
73def show_model_output(parent, fname):
74    # Make sure we have a python file; not sure why we care though...
75    if not (fname and os.path.exists(fname) and fname.endswith('.py')):
76        mssg = "\n This is not a python file."
77        wx.MessageBox(str(mssg), 'Error', style=wx.ICON_ERROR)
78        return False
79
80    try:
81        result, errmsg = check_model(fname), None
82    except Exception:
83        import traceback
84        result, errmsg = None, traceback.format_exc()
85
86    parts = ["Running model '%s'..." % os.path.basename(fname)]
87    if errmsg is not None:
88        parts.extend(["", "Error occurred:", errmsg, ""])
89        title, icon = "Error", wx.ICON_ERROR
90    else:
91        parts.extend(["", "Success:", result, ""])
92        title, icon = "Info", wx.ICON_INFORMATION
93    text = "\n".join(parts)
94    dlg = ResizableScrolledMessageDialog(parent, text, title, size=((550, 250)))
95    fnt = wx.Font(10, wx.TELETYPE, wx.NORMAL, wx.NORMAL)
96    dlg.GetChildren()[0].SetFont(fnt)
97    dlg.GetChildren()[0].SetInsertionPoint(0)
98    dlg.ShowModal()
99    dlg.Destroy()
100    return errmsg is None
101
102class ResizableScrolledMessageDialog(wx.Dialog):
103    """
104    Custom version of wx ScrolledMessageDialog, allowing border resize
105    """
106    def __init__(self, parent, msg, caption,
107        pos=wx.DefaultPosition, size=(500,300),
108        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER ):
109        # Notice, that style can be overrriden in the caller.
110        wx.Dialog.__init__(self, parent, -1, caption, pos, size, style)
111        x, y = pos
112        if x == -1 and y == -1:
113            self.CenterOnScreen(wx.BOTH)
114
115        text = wx.TextCtrl(self, -1, msg, style=wx.TE_MULTILINE | wx.TE_READONLY)
116        ok = wx.Button(self, wx.ID_OK, "OK")
117
118        # Mysterious constraint layouts from
119        # https://www.wxpython.org/docs/api/wx.lib.layoutf.Layoutf-class.html
120        lc = layoutf.Layoutf('t=t5#1;b=t5#2;l=l5#1;r=r5#1', (self,ok))
121        text.SetConstraints(lc)
122        lc = layoutf.Layoutf('b=b5#1;x%w50#1;w!80;h!25', (self,))
123        ok.SetConstraints(lc)
124
125        self.SetAutoLayout(1)
126        self.Layout()
127
128class PyConsole(editor.EditorNotebookFrame):
129    ## Internal nickname for the window, used by the AUI manager
130    window_name = "Custom Model Editor"
131    ## Name to appear on the window title bar
132    window_caption = "Plugin Model Editor"
133    ## Flag to tell the AUI manager to put this panel in the center pane
134    CENTER_PANE = False
135    def __init__(self, parent=None, base=None, manager=None, panel=None,
136                    title='Python Shell/Editor', filename=None,
137                    size=(PANEL_WIDTH, PANEL_HEIGHT)):
138        self.config = None
139
140        editor.EditorNotebookFrame.__init__(self, parent=parent,
141                                        title=title, size=size)
142        self.parent = parent
143        self._manager = manager
144        self.base = base
145        self.panel = panel
146        self._add_menu()
147        if filename is not None:
148            dataDir = os.path.dirname(filename)
149        elif self.parent is not None:
150            dataDir = self.parent._default_save_location
151        else:
152             dataDir = None
153        self.dataDir = dataDir
154        self.Centre()
155
156        # See if there is a corresponding C file
157        if filename is not None:
158            c_filename = os.path.splitext(filename)[0] + ".c"
159            if os.path.isfile(c_filename):
160                self.bufferCreate(c_filename)
161
162            # If not, just open the requested .py, if any.
163            # Needs to be after the C file so the tab focus is correct.
164            if os.path.isfile(filename):
165                    self.bufferCreate(filename)
166
167        self.Bind(wx.EVT_MENU, self.OnNewFile, id=wx.ID_NEW)
168        self.Bind(wx.EVT_MENU, self.OnOpenFile, id=wx.ID_OPEN)
169        self.Bind(wx.EVT_MENU, self.OnSaveFile, id=wx.ID_SAVE)
170        self.Bind(wx.EVT_MENU, self.OnSaveAsFile, id=wx.ID_SAVEAS)
171        self.Bind(wx.EVT_MENU, self.OnCheckModel, id=ID_CHECK_MODEL)
172        self.Bind(wx.EVT_MENU, self.OnRun, id=ID_RUN)
173        self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateCompileMenu, id=ID_CHECK_MODEL)
174        self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateCompileMenu, id=ID_RUN)
175        self.Bind(wx.EVT_CLOSE, self.on_close)
176        if not title.count('Python Shell'):
177            # Delete menu item (open and new) if not python shell
178            #self.fileMenu.Delete(wx.ID_NEW)
179            self.fileMenu.Delete(wx.ID_OPEN)
180
181
182    def _add_menu(self):
183        """
184        Add menu
185        """
186        self.compileMenu = wx.Menu()
187        self.compileMenu.Append(ID_CHECK_MODEL, 'Check model',
188                 'Loading and run the model')
189        self.compileMenu.AppendSeparator()
190        self.compileMenu.Append(ID_RUN, 'Run in Shell',
191                 'Run the file in the Python Shell')
192        self.MenuBar.Insert(3, self.compileMenu, '&Run')
193
194    def OnHelp(self, event):
195        """
196        Show a help dialog.
197        """
198        import  wx.lib.dialogs
199        title = 'Help on key bindings'
200        text = wx.py.shell.HELP_TEXT
201        dlg = wx.lib.dialogs.ScrolledMessageDialog(self, text, title,
202                                                   size=((700, 540)))
203        fnt = wx.Font(10, wx.TELETYPE, wx.NORMAL, wx.NORMAL)
204        dlg.GetChildren()[0].SetFont(fnt)
205        dlg.GetChildren()[0].SetInsertionPoint(0)
206        dlg.ShowModal()
207        dlg.Destroy()
208
209    def set_manager(self, manager):
210        """
211        Set the manager of this window
212        """
213        self._manager = manager
214
215    def OnAbout(self, event):
216        """
217        On About
218        """
219        message = ABOUT
220        dial = wx.MessageDialog(self, message, 'About',
221                           wx.OK | wx.ICON_INFORMATION)
222        dial.ShowModal()
223
224    def OnNewFile(self, event):
225        """
226        OnFileOpen
227        """
228        self.OnFileNew(event)
229
230    def OnOpenFile(self, event):
231        """
232        OnFileOpen
233        """
234        self.OnFileOpen(event)
235        self.Show(False)
236        self.Show(True)
237
238    def OnSaveFile(self, event):
239        """
240        OnFileSave overwrite
241        """
242        self.OnFileSave(event)
243        self.Show(False)
244        self.Show(True)
245
246    def OnSaveAsFile(self, event):
247        """
248        OnFileSaveAs overwrite
249        """
250        self.OnFileSaveAs(event)
251        self.Show(False)
252        self.Show(True)
253
254    def bufferOpen(self):
255        """
256        Open file in buffer, bypassing editor bufferOpen
257        """
258        if self.bufferHasChanged():
259            cancel = self.bufferSuggestSave()
260            if cancel:
261                return cancel
262        filedir = ''
263        if self.buffer and self.buffer.doc.filedir:
264            filedir = self.buffer.doc.filedir
265        if not filedir:
266            filedir = self.dataDir
267        result = editor.openSingle(directory=filedir,
268                            wildcard='Python Files (*.py)|*.py')
269        if result.path:
270            self.bufferCreate(result.path)
271
272        # See if there is a corresponding C file
273        if result.path is not None:
274            c_filename = os.path.splitext(result.path)[0] + ".c"
275            if os.path.isfile(c_filename):
276                self.bufferCreate(c_filename)
277
278        cancel = False
279        return cancel
280
281    def bufferSaveAs(self):
282        """
283        Save buffer to a new filename: Bypassing editor bufferSaveAs
284        """
285        filedir = ''
286        if self.buffer and self.buffer.doc.filedir:
287            filedir = self.buffer.doc.filedir
288        if not filedir:
289            filedir = self.dataDir
290        result = editor.saveSingle(directory=filedir,
291                                   filename='untitled.py',
292                                   wildcard='Python Files (*.py)|*.py')
293        if result.path:
294            self.buffer.confirmed = True
295            self.buffer.saveAs(result.path)
296            cancel = False
297        else:
298            cancel = True
299        return cancel
300
301    def OnRun(self, event):
302        """
303        Run
304        """
305        if not self._check_saved():
306            return True
307        if self.buffer and self.buffer.doc.filepath:
308            self.editor.setFocus()
309            # Why we have to do this (Otherwise problems on Windows)?
310            forward_path = self.buffer.doc.filepath.replace('\\', '/')
311            self.shell.Execute("execfile('%s')" % forward_path)
312            self.shell.Hide()
313            self.shell.Show(True)
314            return self.shell.GetText().split(">>>")[-2]
315        else:
316            mssg = "\n This is not a python file."
317            title = 'Error'
318            icon = wx.ICON_ERROR
319            wx.MessageBox(str(mssg), title, style=icon)
320            return False
321
322    def OnCheckModel(self, event):
323        """
324        Compile
325        """
326        if not self._check_saved():
327            return True
328        fname = self.editor.getStatus()[0]
329        success = show_model_output(self, fname)
330
331        # Update plugin model list in fitpage combobox
332        if success and self._manager is not None and self.panel is not None:
333            self._manager.set_edit_menu_helper(self.parent)
334            wx.CallAfter(self._manager.update_custom_combo)
335
336    def _check_saved(self):
337        """
338        If content was changed, suggest to save it first
339        """
340        if self.bufferHasChanged() and self.buffer.doc.filepath:
341            cancel = self.bufferSuggestSave()
342            return not cancel
343        return True
344
345    def OnUpdateCompileMenu(self, event):
346        """
347        Update Compile menu items based on current tap.
348        """
349        win = wx.Window.FindFocus()
350        id = event.GetId()
351        event.Enable(True)
352        try:
353            if id == ID_CHECK_MODEL or id == ID_RUN:
354                menu_on = False
355                if self.buffer and self.buffer.doc.filepath:
356                    menu_on = True
357                event.Enable(menu_on)
358        except AttributeError:
359            # This menu option is not supported in the current context.
360            event.Enable(False)
361
362    def on_close(self, event):
363        """
364        Close event
365        """
366        if self.base is not None:
367            self.base.py_frame = None
368        self.Destroy()
369
370ABOUT = "Welcome to Python %s! \n\n" % sys.version.split()[0]
371ABOUT += "This uses Py Shell/Editor in wx (developed by Patrick K. O'Brien).\n"
372ABOUT += "If this is your first time using Python, \n"
373ABOUT += "you should definitely check out the tutorial "
374ABOUT += "on the Internet at http://www.python.org/doc/tut/."
375
376
377if __name__ == "__main__":
378
379    app = wx.App()
380    dlg = PyConsole()
381    dlg.Show()
382    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.