| 1 | # shell.py |
| 2 | #---------------------------------------------------------------------- |
| 3 | # 12/10/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
| 4 | # |
| 5 | # o 2.5 compatability update. |
| 6 | # o Added deprecation warning. |
| 7 | # |
| 8 | |
| 9 | """wxPython interactive shell |
| 10 | |
| 11 | Copyright (c) 1999 SIA "ANK" |
| 12 | |
| 13 | this module is free software. it may be used under same terms as Python itself |
| 14 | |
| 15 | Notes: |
| 16 | i would like to use command completion (see rlcompleter library module), |
| 17 | but i cannot load it because i don't have readline... |
| 18 | |
| 19 | History: |
| 20 | 03-oct-1999 [als] created |
| 21 | 04-oct-1999 [als] PyShellOutput.intro moved from __init__ parameters |
| 22 | to class attributes; html debug disabled |
| 23 | 04-oct-1999 [als] fixed bug with class attributes |
| 24 | input prompts and output styles added to customized demo |
| 25 | some html cleanups |
| 26 | 04-oct-1999 [rpd] Changed to use the new sizers |
| 27 | 05-oct-1999 [als] changes inspired by code.InteractiveInterpreter() |
| 28 | from Python Library. if i knew about this class earlier, |
| 29 | i would rather inherit from it. |
| 30 | renamed to wxPyShell.py since i've renounced the 8.3 scheme |
| 31 | |
| 32 | 8-10-2001 THIS MODULE IS NOW DEPRECATED. Please see the most excellent |
| 33 | PyCrust package instead. |
| 34 | |
| 35 | """ |
| 36 | __version__ ="$Revision$" |
| 37 | # $RCSfile$ |
| 38 | |
| 39 | import code |
| 40 | import sys |
| 41 | import traceback |
| 42 | import warnings |
| 43 | |
| 44 | import wx |
| 45 | import wx.html |
| 46 | |
| 47 | warningmsg = r"""\ |
| 48 | |
| 49 | ########################################\ |
| 50 | # THIS MODULE IS NOW DEPRECATED | |
| 51 | # | |
| 52 | # Please see the most excellent PyCrust | |
| 53 | # package instead. | |
| 54 | ########################################/ |
| 55 | |
| 56 | """ |
| 57 | |
| 58 | warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) |
| 59 | |
| 60 | #---------------------------------------------------------------------- |
| 61 | |
| 62 | class PyShellInput(wx.Panel): |
| 63 | """PyShell input window |
| 64 | |
| 65 | """ |
| 66 | PS1 =" Enter Command:" |
| 67 | PS2 ="... continue:" |
| 68 | def __init__(self, parent, shell, id=-1): |
| 69 | """Create input window |
| 70 | |
| 71 | shell must be a PyShell object. |
| 72 | it is used for exception handling, eval() namespaces, |
| 73 | and shell.output is used for output |
| 74 | (print's go to overridden stdout) |
| 75 | """ |
| 76 | wx.Panel.__init__(self, parent, id) |
| 77 | self.shell =shell |
| 78 | # make a private copy of class attrs |
| 79 | self.PS1 =PyShellInput.PS1 |
| 80 | self.PS2 =PyShellInput.PS2 |
| 81 | # create controls |
| 82 | self.label =wx.StaticText(self, -1, self.PS1) |
| 83 | tid =wx.NewId() |
| 84 | self.entry =wx.TextCtrl(self, tid, style = wx.TE_MULTILINE) |
| 85 | self.entry.Bind(wx.EVT_CHAR, self.OnChar) |
| 86 | self.entry.SetFont(wx.Font(9, wx.MODERN, wx.NORMAL, wx.NORMAL, False)) |
| 87 | sizer =wx.BoxSizer(wx.VERTICAL) |
| 88 | sizer.AddMany([(self.label, 0, wx.EXPAND), (self.entry, 1, wx.EXPAND)]) |
| 89 | self.SetSizer(sizer) |
| 90 | self.SetAutoLayout(True) |
| 91 | self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) |
| 92 | # when in "continuation" mode, |
| 93 | # two consecutive newlines are required |
| 94 | # to avoid execution of unfinished block |
| 95 | self.first_line =1 |
| 96 | |
| 97 | def OnSetFocus(self, event): |
| 98 | self.entry.SetFocus() |
| 99 | |
| 100 | |
| 101 | def Clear(self, event=None): |
| 102 | """reset input state""" |
| 103 | self.label.SetLabel(self.PS1) |
| 104 | self.label.Refresh() |
| 105 | self.entry.SetSelection(0, self.entry.GetLastPosition()) |
| 106 | self.first_line =1 |
| 107 | # self.entry.SetFocus() |
| 108 | |
| 109 | def OnChar(self, event): |
| 110 | """called on CHARevent. executes input on newline""" |
| 111 | # print "On Char:", event.__dict__.keys() |
| 112 | if event.GetKeyCode() !=wx.WXK_RETURN: |
| 113 | # not of our business |
| 114 | event.Skip() |
| 115 | return |
| 116 | text =self.entry.GetValue() |
| 117 | # weird CRLF thingy |
| 118 | text = text.replace("\r\n", "\n") |
| 119 | # see if we've finished |
| 120 | if (not (self.first_line or text[-1] =="\n") # in continuation mode |
| 121 | or (text[-1] =="\\") # escaped newline |
| 122 | ): |
| 123 | # XXX should escaped newline put myself i "continuation" mode? |
| 124 | event.Skip() |
| 125 | return |
| 126 | # ok, we can try to execute this |
| 127 | rc =self.shell.TryExec(text) |
| 128 | if rc: |
| 129 | # code is incomplete; continue input |
| 130 | if self.first_line: |
| 131 | self.label.SetLabel(self.PS2) |
| 132 | self.label.Refresh() |
| 133 | self.first_line =0 |
| 134 | event.Skip() |
| 135 | else: |
| 136 | self.Clear() |
| 137 | |
| 138 | class PyShellOutput(wx.Panel): |
| 139 | """PyShell output window |
| 140 | |
| 141 | for now, it is based on simple wxTextCtrl, |
| 142 | but i'm looking at HTML classes to provide colorized output |
| 143 | """ |
| 144 | # attributes for for different (input, output, exception) display styles: |
| 145 | # begin tag, end tag, newline |
| 146 | in_style =(" <font color=\"#000080\"><tt>>>> ", |
| 147 | "</tt></font><br>\n", "<br>\n... ") |
| 148 | out_style =("<tt>", "</tt>\n", "<br>\n") |
| 149 | exc_style =("<font color=\"#FF0000\"><tt>", |
| 150 | "</tt></font>\n", "<br>\n") |
| 151 | intro ="<H3>wxPython Interactive Shell</H3>\n" |
| 152 | html_debug =0 |
| 153 | # entity references |
| 154 | erefs =(("&", "&"), (">", ">"), ("<", "<"), (" ", " ")) |
| 155 | def __init__(self, parent, id=-1): |
| 156 | wx.Panel.__init__(self, parent, id) |
| 157 | # make a private copy of class attrs |
| 158 | self.in_style =PyShellOutput.in_style |
| 159 | self.out_style =PyShellOutput.out_style |
| 160 | self.exc_style =PyShellOutput.exc_style |
| 161 | self.intro =PyShellOutput.intro |
| 162 | self.html_debug =PyShellOutput.html_debug |
| 163 | # create windows |
| 164 | if self.html_debug: |
| 165 | # this was used in html debugging, |
| 166 | # but i don't want to delete it; it's funny |
| 167 | splitter =wx.SplitterWindow(self, -1) |
| 168 | self.view =wx.TextCtrl(splitter, -1, |
| 169 | style = wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL) |
| 170 | self.html =wx.html.HtmlWindow(splitter) |
| 171 | splitter.SplitVertically(self.view, self.html) |
| 172 | splitter.SetSashPosition(40) |
| 173 | splitter.SetMinimumPaneSize(3) |
| 174 | self.client =splitter |
| 175 | else: |
| 176 | self.view =None |
| 177 | self.html =wx.html.HtmlWindow(self) |
| 178 | self.client =self.html # used in OnSize() |
| 179 | self.text =self.intro |
| 180 | self.html.SetPage(self.text) |
| 181 | self.html.SetAutoLayout(True) |
| 182 | self.line_buffer ="" |
| 183 | # refreshes are annoying |
| 184 | self.in_batch =0 |
| 185 | self.dirty =0 |
| 186 | self.Bind(wx.EVT_SIZE, self.OnSize) |
| 187 | self.Bind(wx.EVT_IDLE, self.OnIdle) |
| 188 | |
| 189 | def OnSize(self, event): |
| 190 | self.client.SetSize(self.GetClientSize()) |
| 191 | |
| 192 | def OnIdle(self, event): |
| 193 | """when there's nothing to do, we can update display""" |
| 194 | if self.in_batch and self.dirty: self.UpdWindow() |
| 195 | |
| 196 | def BeginBatch(self): |
| 197 | """do not refresh display till EndBatch()""" |
| 198 | self.in_batch =1 |
| 199 | |
| 200 | def EndBatch(self): |
| 201 | """end batch; start updating display immediately""" |
| 202 | self.in_batch =0 |
| 203 | if self.dirty: self.UpdWindow() |
| 204 | |
| 205 | def UpdWindow(self): |
| 206 | """sync display with text buffer""" |
| 207 | html =self.html |
| 208 | html.SetPage(self.text) |
| 209 | self.dirty =0 |
| 210 | # scroll to the end |
| 211 | (x,y) =html.GetVirtualSize() |
| 212 | html.Scroll(0, y) |
| 213 | |
| 214 | def AddText(self, text, style=None): |
| 215 | """write text to output window""" |
| 216 | # a trick needed to defer default from compile-time to execute-time |
| 217 | if style ==None: style =self.out_style |
| 218 | if 0 and __debug__: sys.__stdout__.write(text) |
| 219 | # handle entities |
| 220 | for (symbol, eref) in self.erefs: |
| 221 | text = text.replace(symbol, eref) |
| 222 | # replace newlines |
| 223 | text = text.replace("\n", style[2]) |
| 224 | # add to contents |
| 225 | self.text =self.text +style[0] +text +style[1] |
| 226 | if not self.in_batch: self.UpdWindow() |
| 227 | else: self.dirty =1 |
| 228 | if self.html_debug: |
| 229 | # html debug output needn't to be too large |
| 230 | self.view.SetValue(self.text[-4096:]) |
| 231 | |
| 232 | def write(self, str, style=None): |
| 233 | """stdout-like interface""" |
| 234 | if style ==None: style =self.out_style |
| 235 | # do not process incomplete lines |
| 236 | if len(str) <1: |
| 237 | # hm... what was i supposed to do? |
| 238 | return |
| 239 | elif str[-1] !="\n": |
| 240 | self.line_buffer =self.line_buffer +str |
| 241 | else: |
| 242 | self.AddText(self.line_buffer +str, style) |
| 243 | self.line_buffer ="" |
| 244 | |
| 245 | def flush(self, style=None): |
| 246 | """write out all that was left in line buffer""" |
| 247 | if style ==None: style =self.out_style |
| 248 | self.AddText(self.line_buffer +"\n", style) |
| 249 | |
| 250 | def write_in(self, str, style=None): |
| 251 | """write text in "input" style""" |
| 252 | if style ==None: style =self.in_style |
| 253 | self.AddText(str, style) |
| 254 | |
| 255 | def write_exc(self, str, style=None): |
| 256 | """write text in "exception" style""" |
| 257 | if style ==None: style =self.exc_style |
| 258 | self.AddText(str, style) |
| 259 | |
| 260 | class PyShell(wx.Panel): |
| 261 | """interactive Python shell with wxPython interface |
| 262 | |
| 263 | """ |
| 264 | def __init__(self, parent, globals=globals(), locals={}, |
| 265 | id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, |
| 266 | style=wx.TAB_TRAVERSAL, name="shell"): |
| 267 | """create PyShell window""" |
| 268 | wx.Panel.__init__(self, parent, id, pos, size, style, name) |
| 269 | self.globals =globals |
| 270 | self.locals =locals |
| 271 | splitter =wx.SplitterWindow(self, -1) |
| 272 | self.output =PyShellOutput(splitter) |
| 273 | self.input =PyShellInput(splitter, self) |
| 274 | self.input.SetFocus() |
| 275 | splitter.SplitHorizontally(self.input, self.output) |
| 276 | splitter.SetSashPosition(100) |
| 277 | splitter.SetMinimumPaneSize(20) |
| 278 | self.splitter =splitter |
| 279 | self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) |
| 280 | self.Bind(wx.EVT_SIZE, self.OnSize) |
| 281 | |
| 282 | def OnSetFocus(self, event): |
| 283 | self.input.SetFocus() |
| 284 | |
| 285 | def TryExec(self, source, symbol="single"): |
| 286 | """Compile and run some source in the interpreter. |
| 287 | |
| 288 | borrowed from code.InteractiveInterpreter().runsource() |
| 289 | as i said above, i would rather like to inherit from that class |
| 290 | |
| 291 | returns 1 if more input is required, or 0, otherwise |
| 292 | """ |
| 293 | try: |
| 294 | cc = code.compile_command(source, symbol=symbol) |
| 295 | except (OverflowError, SyntaxError): |
| 296 | # [als] hm... never seen anything of that kind |
| 297 | self.ShowSyntaxError() |
| 298 | return 0 |
| 299 | if cc is None: |
| 300 | # source is incomplete |
| 301 | return 1 |
| 302 | # source is sucessfully compiled |
| 303 | out =self.output |
| 304 | # redirect system stdout to the output window |
| 305 | prev_out =sys.stdout |
| 306 | sys.stdout =out |
| 307 | # begin printout batch (html updates are deferred until EndBatch()) |
| 308 | out.BeginBatch() |
| 309 | out.write_in(source) |
| 310 | try: |
| 311 | exec cc in self.globals, self.locals |
| 312 | except SystemExit: |
| 313 | # SystemExit is not handled and has to be re-raised |
| 314 | raise |
| 315 | except: |
| 316 | # all other exceptions produce traceback output |
| 317 | self.ShowException() |
| 318 | # switch back to saved stdout |
| 319 | sys.stdout =prev_out |
| 320 | # commit printout |
| 321 | out.flush() |
| 322 | out.EndBatch() |
| 323 | return 0 |
| 324 | |
| 325 | def ShowException(self): |
| 326 | """display the traceback for the latest exception""" |
| 327 | (etype, value, tb) =sys.exc_info() |
| 328 | # remove myself from traceback |
| 329 | tblist =traceback.extract_tb(tb)[1:] |
| 330 | msg = ' '.join(traceback.format_exception_only(etype, value) |
| 331 | +traceback.format_list(tblist)) |
| 332 | self.output.write_exc(msg) |
| 333 | |
| 334 | def ShowSyntaxError(self): |
| 335 | """display message about syntax error (no traceback here)""" |
| 336 | (etype, value, tb) =sys.exc_info() |
| 337 | msg = ' '.join(traceback.format_exception_only(etype, value)) |
| 338 | self.output.write_exc(msg) |
| 339 | |
| 340 | def OnSize(self, event): |
| 341 | self.splitter.SetSize(self.GetClientSize()) |
| 342 | |
| 343 | #---------------------------------------------------------------------- |
| 344 | if __name__ == '__main__': |
| 345 | class MyFrame(wx.Frame): |
| 346 | """Very standard Frame class. Nothing special here!""" |
| 347 | def __init__(self, parent=None, id =-1, |
| 348 | title="wxPython Interactive Shell"): |
| 349 | wx.Frame.__init__(self, parent, id, title) |
| 350 | self.shell =PyShell(self) |
| 351 | |
| 352 | class MyApp(wx.App): |
| 353 | """Demonstrates usage of both default and customized shells""" |
| 354 | def OnInit(self): |
| 355 | frame = MyFrame() |
| 356 | frame.Show(True) |
| 357 | self.SetTopWindow(frame) |
| 358 | ## PyShellInput.PS1 =" let's get some work done..." |
| 359 | ## PyShellInput.PS2 =" ok, what do you really mean?" |
| 360 | ## PyShellOutput.in_style =( |
| 361 | ## "<I><font color=\"#008000\"><tt>>>> ", |
| 362 | ## "</tt></font></I><br>\n", "<br>\n... ") |
| 363 | ## PyShellOutput.out_style =( |
| 364 | ## "<font color=\"#000080\"><tt>", |
| 365 | ## "</tt></font><br>\n", "<br>\n") |
| 366 | ## PyShellOutput.exc_style =("<B><font color=\"#FF0000\"><tt>", |
| 367 | ## "</tt></font></B>\n", "<br>\n") |
| 368 | ## PyShellOutput.intro ="<I><B>Customized wxPython Shell</B>" \ |
| 369 | ## "<br><-- move this sash to see html debug output</I><br>\n" |
| 370 | ## PyShellOutput.html_debug =1 |
| 371 | ## frame = MyFrame(title="Customized wxPython Shell") |
| 372 | ## frame.Show(True) |
| 373 | return True |
| 374 | |
| 375 | app = MyApp(0) |
| 376 | app.MainLoop() |
| 377 | |