1 """A Tkinter-based console for conversing with the Python interpreter,
2 featuring more tolerant pasting of code from other interactive sessions,
3 better handling of continuations than the standard Python interpreter,
4 highlighting of the most recently-executed code block, the ability to
5 edit and reexecute previously entered code, a history of recently-entered
6 lines, automatic multi-level completion with pop-up menus, and pop-up help.
7
8 Ka-Ping Yee <ping@lfw.org>, 18 April 1999. This software is in the public
9 domain and is provided without express or implied warranty. Permission to
10 use, modify, or distribute the software for any purpose is hereby granted."""
11
12
13
14
15
16
17
18 from Tkinter import *
19 import sys, string, traceback, types, __builtin__
20
21 REVISION = "$Revision: 1.1.1.4 $"
22 VERSION = string.split(REVISION)[1]
23
25 """A substitute file object for redirecting output to a function."""
26
28 self.writer = writer
29 self.closed = 0
30
32 return "<OutputPipe to %s>" % repr(self.writer)
33
34 - def read(self, length):
35 return ""
36
38 if not self.closed: self.writer(data)
39
41 self.closed = 1
42
43
45 - def __init__(self, parent=None, dict={}, **options):
46 """Construct from a parent widget, an optional dictionary to use
47 as the namespace for execution, and any configuration options."""
48 Frame.__init__(self, parent)
49
50
51
52 self.continuation = 0
53 self.error = 0
54 self.intraceback = 0
55 self.pasted = 0
56
57
58
59 self.history = []
60 self.historyindex = None
61 self.current = ""
62
63
64
65 self.compmenus = []
66 self.compindex = None
67 self.compfinish = ""
68
69
70
71 self.stdout = OutputPipe(lambda data, w=self.write: w(data, "stdout"))
72 self.stderr = OutputPipe(lambda data, w=self.write: w(data, "stderr"))
73
74
75
76 if not hasattr(sys, "ps1"): sys.ps1 = ">>> "
77 if not hasattr(sys, "ps2"): sys.ps2 = "... "
78 self.prefixes = [sys.ps1, sys.ps2, ">> ", "> "]
79 self.startup = "Python %s\n%s\n" % (sys.version, sys.copyright) + \
80 "Python Console v%s by Ka-Ping Yee <ping@lfw.org>\n" % VERSION
81 self.dict = dict
82
83
84
85 self.text = Text(self, insertontime=200, insertofftime=150)
86 self.text.insert("end", self.startup)
87 self.text.insert("end", sys.ps1)
88 self.text.bind("<Return>", self.cb_return)
89 self.text.bind("<Button-1>", self.cb_select)
90 self.text.bind("<ButtonRelease-1>", self.cb_position)
91 self.text.bind("<ButtonRelease-2>", self.cb_paste)
92 self.text.bind("<Home>", self.cb_home)
93 self.text.bind("<Control-Home>", self.cb_ctrlhome)
94 self.text.bind("<Up>", self.cb_back)
95 self.text.bind("<Down>", self.cb_forward)
96 self.text.bind("<Configure>", self.cb_cleanup)
97 self.text.bind("<Expose>", self.cb_cleanup)
98 self.text.bind("<Key>", self.cb_cleanup)
99 self.text.bind("<Tab>", self.cb_complete)
100 self.text.bind("<Left>", self.cb_position)
101 self.text.bind("<space>", self.cb_space)
102 self.text.bind("<BackSpace>", self.cb_backspace)
103 self.text.bind("<KeyRelease-BackSpace>", self.cb_nothing)
104 self.text.bind("<F1>", self.cb_help)
105 self.text.bind("<Control-slash>", self.cb_help)
106 self.text.bind("<Alt-h>", self.cb_help)
107
108
109
110 self.scroll = Scrollbar(self, command=self.text.yview)
111 self.text.config(yscrollcommand=self.scroll.set)
112 self.scroll.pack(side=RIGHT, fill=Y)
113 self.text.pack(fill=BOTH, expand=1)
114 self.text.focus()
115
116
117
118 self.options = {"stdoutcolour": "#7020c0",
119 "stderrcolour": "#c03020",
120 "morecolour": "#a0d0f0",
121 "badcolour": "#e0b0b0",
122 "runcolour": "#90d090"}
123 apply(self.config, (), self.options)
124 apply(self.config, (), options)
125
127 return self.options[key]
128
130 if not self.options.has_key(key):
131 raise KeyError, 'no such configuration option "%s"' % key
132 self.options[key] = value
133 if key == "stdoutcolour":
134 self.text.tag_configure("stdout", foreground=value)
135 if key == "stderrcolour":
136 self.text.tag_configure("stderr", foreground=value)
137
138 - def config(self, *args, **dict):
139 """Get or set configuration options in a Tkinter-like style."""
140 if args == () and dict == {}:
141 return self.options
142 if len(args) == 1:
143 return self.options[args[0]]
144 for key, value in dict.items():
145 self[key] = value
146
147
148
149 - def trim(self, command):
150 """Trim any matching prefix from the given command line, returning
151 the amount trimmed and the trimmed result."""
152 for prefix in self.prefixes:
153 if command[:len(prefix)] == prefix:
154 return len(prefix), command[len(prefix):]
155 return 0, command
156
157 - def getline(self, line=None, trim=0):
158 """Return the command on the current line."""
159 if line is None:
160 line, pos = self.cursor()
161 command = self.text.get("%d.0" % line, "%d.end" % line)
162 if trim:
163 trimmed, command = self.trim(command)
164 return command
165
167 """Get the current line and position of the cursor."""
168 cursor = self.text.index("insert")
169 [line, pos] = map(string.atoi, string.split(cursor, "."))
170 return line, pos
171
172 - def write(self, data, tag=None):
173 """Show output from stdout or stderr in the console."""
174 if self.intraceback and data[-2:] == "\n ": data = data[:-1]
175 start = self.text.index("insert")
176 self.text.insert("insert", data)
177 end = self.text.index("insert")
178 if tag: self.text.tag_add(tag, start, end)
179
180
181
183 """Step back in the history."""
184 if self.history:
185 if self.historyindex == None:
186 self.current = self.getline(trim=1)
187 self.historyindex = len(self.history) - 1
188 elif self.historyindex > 0:
189 self.historyindex = self.historyindex - 1
190 self.recall()
191
192 return "break"
193
195 """Step forward in the history."""
196 if self.history and self.historyindex is not None:
197 self.historyindex = self.historyindex + 1
198 if self.historyindex < len(self.history):
199 self.recall()
200 else:
201 self.historyindex = None
202 self.recall(self.current)
203
204 return "break"
205
206 - def recall(self, command=None):
207 """Show a command from the history on the current line."""
208 if command is None:
209 command = self.history[self.historyindex]
210 line, pos = self.cursor()
211 current = self.getline(line)
212 trimmed, trimmedline = self.trim(current)
213 cutpos = "%d.%d" % (line, trimmed)
214 self.text.delete(cutpos, "%d.end" % line)
215 self.text.insert(cutpos, command)
216 self.text.mark_set("insert", "%d.end" % line)
217
218
219
220 - def precontext(self):
221
222 line, pos = self.cursor()
223 command = self.getline()
224 preceding = command[:pos]
225 startchars = string.letters + "_"
226 identchars = string.letters + string.digits + "_"
227 while pos > 0 and preceding[pos-1] in identchars:
228 pos = pos - 1
229 preceding, ident = preceding[:pos], preceding[pos:]
230 start = "%d.%d" % (line, pos)
231
232 preceding = string.strip(preceding)
233 context = ""
234 if not ident or ident[0] in startchars:
235
236 while preceding[-1:] == ".":
237 preceding = string.strip(preceding[:-1])
238 if preceding[-1] in identchars:
239 pos = len(preceding)-1
240 while pos > 0 and preceding[pos-1] in identchars:
241 pos = pos - 1
242 if preceding[pos] in startchars:
243 context = preceding[pos:] + "." + context
244 preceding = string.strip(preceding[:pos])
245 else: break
246 else: break
247
248 line, pos = self.cursor()
249 endpos = pos
250 while endpos < len(command) and command[endpos] in identchars:
251 endpos = endpos + 1
252 end = "%d.%d" % (line, endpos)
253
254 return command, context, ident, start, end
255
257 """Attempt to complete the identifier currently being typed."""
258 if self.compmenus:
259 if self.cursor() == self.compindex:
260
261 self.text.insert("insert", self.compfinish)
262 self.compindex = None
263 self.unpostmenus()
264 return "break"
265
266 command, context, ident, start, end = self.precontext()
267
268
269 if context:
270 try:
271 object = eval(context[:-1], self.dict)
272 keys = members(object)
273 except:
274 object = None
275 keys = []
276 else:
277 class Lookup:
278 def __init__(self, dicts):
279 self.dicts = dicts
280
281 def __getattr__(self, key):
282 for dict in self.dicts:
283 if dict.has_key(key): return dict[key]
284 return None
285 object = Lookup([self.dict, __builtin__.__dict__])
286 keys = self.dict.keys() + dir(__builtin__)
287
288 keys = matchingkeys(keys, ident)
289 if not ident:
290 public = []
291 for key in keys:
292 if key[:1] != "_": public.append(key)
293 keys = public
294 skip = len(ident)
295
296
297 if len(keys) == 1:
298
299 if self.cursor() == self.compindex:
300
301 self.text.insert("insert", self.compfinish)
302 self.compindex = None
303 else:
304 self.text.delete("insert", end)
305 self.text.insert("insert", keys[0][skip:])
306 try: self.compfinish = finisher(getattr(object, keys[0]))
307 except: self.compfinish = " "
308 if self.compfinish == " ":
309
310 self.text.insert("insert", " ")
311 else:
312 self.compindex = self.cursor()
313 elif len(keys) > 1:
314
315 prefix = commonprefix(keys)
316 keys.sort()
317 if len(prefix) > skip:
318 self.text.delete("insert", end)
319 self.text.insert("insert", keys[0][skip:len(prefix)])
320 skip = len(prefix)
321
322 if len(keys[0]) == skip:
323
324 self.compindex = self.cursor()
325 try: self.compfinish = finisher(getattr(object, keys[0]))
326 except: self.compfinish = " "
327
328 self.postmenus(keys, skip, end, object)
329
330 return "break"
331
333 """Post a series of menus listing all the given keys, given the
334 length of the existing part so we can position the menus under the
335 cursor, and the index at which to insert the completion."""
336 width = self.winfo_screenwidth()
337 height = self.winfo_screenheight()
338 bbox = self.text.bbox("insert - %d c" % skip)
339 x = self.text.winfo_rootx() + bbox[0] - 4
340 y = self.text.winfo_rooty() + bbox[1] + bbox[3]
341
342 self.compmenus = []
343 menufont = self.text.cget("font")
344 menu = Menu(font=menufont, bd=1, tearoff=0)
345 self.compmenus.append(menu)
346 while keys:
347 try: finishchar = finisher(getattr(object, keys[0]))
348 except: finishchar = " "
349 def complete(s=self, k=keys[0][skip:], c=cut, f=finishchar):
350 if f == " ": k = k + f
351 s.text.delete("insert", c)
352 s.text.insert("insert", k)
353 s.unpostmenus()
354 if f != " ":
355 s.compfinish = f
356 s.compindex = s.cursor()
357 menu.add_command(label=keys[0], command=complete)
358 menu.update()
359 if y + menu.winfo_reqheight() >= height:
360 menu.delete("end")
361 x = x + menu.winfo_reqwidth()
362 y = 0
363 menu = Menu(font=menufont, bd=1, tearoff=0)
364 self.compmenus.append(menu)
365 else:
366 keys = keys[1:]
367 if x + menu.winfo_reqwidth() > width:
368 menu.destroy()
369 self.compmenus = self.compmenus[:-1]
370 self.compmenus[-1].delete("end")
371 self.compmenus[-1].add_command(label="...")
372 break
373
374 x = self.text.winfo_rootx() + bbox[0] - 4
375 y = self.text.winfo_rooty() + bbox[1] + bbox[3]
376 for menu in self.compmenus:
377 maxtop = height - menu.winfo_reqheight()
378 if y > maxtop: y = maxtop
379 menu.post(x, y)
380 x = x + menu.winfo_reqwidth()
381 self.text.focus()
382 self.text.grab_set()
383
385 """Unpost the completion menus."""
386 for menu in self.compmenus:
387 menu.destroy()
388 self.compmenus = []
389 self.text.grab_release()
390
392 if self.compmenus:
393 self.unpostmenus()
394 if self.pasted:
395 self.text.tag_remove("sel", "1.0", "end")
396 self.pasted = 0
397
399 """Handle a menu selection event. We have to check and invoke the
400 completion menus manually because we are grabbing events to give the
401 text box keyboard focus."""
402 if self.compmenus:
403 for menu in self.compmenus:
404 x, y = menu.winfo_rootx(), menu.winfo_rooty()
405 w, h = menu.winfo_width(), menu.winfo_height()
406 if x < event.x_root < x + w and \
407 y < event.y_root < y + h:
408 item = menu.index("@%d" % (event.y_root - y))
409 menu.invoke(item)
410 break
411 else:
412 self.unpostmenus()
413 return "break"
414
415
416
418 command, context, ident, start, end = self.precontext()
419 word = self.text.get(start, end)
420
421 object = parent = doc = None
422 skip = 0
423
424 try:
425 parent = eval(context[:-1], self.dict)
426 except: pass
427
428
429 if not object:
430 try:
431 object = getattr(parent, word)
432 skip = len(word) - len(ident)
433 except: pass
434
435 if not object:
436 try:
437 object = getattr(parent, ident)
438 except: pass
439
440 if not object:
441 try:
442 object = self.dict[word]
443 skip = len(word) - len(ident)
444 except: pass
445
446 if not object:
447 try:
448 object = self.dict[ident]
449 except: pass
450
451 if not object:
452 try:
453 object = __builtin__.__dict__[word]
454 skip = len(word) - len(ident)
455 except: pass
456
457 if not object:
458 try:
459 object = __builtins__.__dict__[ident]
460 except: pass
461
462 if not object:
463 if not ident:
464 object = parent
465
466 try:
467 doc = object.__doc__
468 except: pass
469
470 try:
471 if hasattr(object, "__bases__"):
472 doc = object.__init__.__doc__ or doc
473 except: pass
474
475 if doc:
476 doc = string.rstrip(string.expandtabs(doc))
477 leftmargin = 99
478 for line in string.split(doc, "\n")[1:]:
479 spaces = len(line) - len(string.lstrip(line))
480 if line and spaces < leftmargin: leftmargin = spaces
481
482 bbox = self.text.bbox("insert + %d c" % skip)
483 width = self.winfo_screenwidth()
484 height = self.winfo_screenheight()
485 menufont = self.text.cget("font")
486
487 help = Menu(font=menufont, bd=1, tearoff=0)
488 try:
489 classname = object.__class__.__name__
490 help.add_command(label="<object of class %s>" % classname)
491 help.add_command(label="")
492 except: pass
493 for line in string.split(doc, "\n"):
494 if string.strip(line[:leftmargin]) == "":
495 line = line[leftmargin:]
496 help.add_command(label=line)
497 self.compmenus.append(help)
498
499 x = self.text.winfo_rootx() + bbox[0] - 4
500 y = self.text.winfo_rooty() + bbox[1] + bbox[3]
501 maxtop = height - help.winfo_reqheight()
502 if y > maxtop: y = maxtop
503 help.post(x, y)
504 self.text.focus()
505 self.text.grab_set()
506
507 return "break"
508
509
510
512 """Avoid moving into the prompt area."""
513 self.cb_cleanup()
514 line, pos = self.cursor()
515 trimmed, command = self.trim(self.getline())
516 if pos <= trimmed:
517 self.text.mark_set("insert", "%d.%d" % (line, trimmed))
518 return "break"
519
521 self.cb_cleanup()
522 if self.text.tag_ranges("sel"): return
523
524
525 line, pos = self.cursor()
526 trimmed, command = self.trim(self.getline())
527 if pos <= trimmed: return "break"
528
529
530 if not string.strip(command[:pos-trimmed]):
531 step = (pos - trimmed) % 4
532 cut = pos - (step or 4)
533 if cut < trimmed: cut = trimmed
534 self.text.delete("%d.%d" % (line, cut), "%d.%d" % (line, pos))
535 return "break"
536
538 self.cb_cleanup()
539 line, pos = self.cursor()
540 trimmed, command = self.trim(self.getline())
541
542
543 if not string.strip(command[:pos-trimmed]):
544 start = trimmed + len(command) - len(string.lstrip(command))
545 self.text.delete("insert", "%d.%d" % (line, start))
546 step = 4 - (pos - trimmed) % 4
547 self.text.insert("insert", " " * step)
548 return "break"
549
551 """Go to the first non-whitespace character in the line."""
552 self.cb_cleanup()
553 line, pos = self.cursor()
554 trimmed, command = self.trim(self.getline())
555 indent = len(command) - len(string.lstrip(command))
556 self.text.mark_set("insert", "%d.%d" % (line, trimmed + indent))
557 return "break"
558
560 """Go to the beginning of the line just after the prompt."""
561 self.cb_cleanup()
562 line, pos = self.cursor()
563 trimmed, command = self.trim(self.getline())
564 self.text.mark_set("insert", "%d.%d" % (line, trimmed))
565 return "break"
566
568 return "break"
569
571 """Handle a <Return> keystroke by running from the current line
572 and generating a new prompt."""
573 self.cb_cleanup()
574 self.text.tag_delete("compiled")
575 self.historyindex = None
576 command = self.getline(trim=1)
577 if string.strip(command):
578 self.history.append(command)
579
580 line, pos = self.cursor()
581 self.text.mark_set("insert", "%d.end" % line)
582 self.text.insert("insert", "\n")
583 self.runline(line)
584
585 line, pos = self.cursor()
586 self.text.mark_set("insert", "%d.end" % line)
587 prompt = self.continuation and sys.ps2 or sys.ps1
588 if pos > 0:
589 self.text.insert("insert", "\n" + prompt)
590 else:
591 self.text.insert("insert", prompt)
592
593 if doindent and not self.error:
594 self.autoindent(command)
595 self.error = 0
596 self.text.see("insert")
597 return "break"
598
600
601 indent = len(command) - len(string.lstrip(command))
602 if string.lstrip(command):
603 self.text.insert("insert", command[:indent])
604 if string.rstrip(command)[-1] == ":":
605 self.text.insert("insert", " ")
606
608 """Handle a paste event (middle-click) in the text box. Pasted
609 text has any leading Python prompts stripped (at last!!)."""
610 self.text.tag_delete("compiled")
611 self.error = 0
612 self.pasted = 1
613
614 try: lines = string.split(self.selection_get(), "\n")
615 except: return
616
617 for i in range(len(lines)):
618 trimmed, line = self.trim(lines[i])
619 line = string.rstrip(line)
620 if not line: continue
621
622 self.text.insert("end", line)
623 self.text.mark_set("insert", "end")
624 if i == len(lines) - 2 and lines[i+1] == "":
625
626 self.cb_return(None, doindent=1)
627 elif i < len(lines) - 1:
628 self.cb_return(None, doindent=0)
629
630 if self.error: break
631
632 return "break"
633
634
635
637 """Run some source code given the number of the last line in the
638 text box. Scan backwards to get the entire piece of code to run
639 if the line is a continuation of previous lines. Tag the compiled
640 code so that it can be highlighted according to whether it is
641 complete, incomplete, or illegal."""
642 lastline = line
643 lines = [self.getline(line)]
644 while lines[0][:len(sys.ps2)] == sys.ps2:
645 trimmed, lines[0] = self.trim(lines[0])
646 self.text.tag_add(
647 "compiled", "%d.%d" % (line, trimmed), "%d.0" % (line+1))
648 line = line - 1
649 if line < 0: break
650 lines[:0] = [self.getline(line)]
651 if lines[0][:len(sys.ps1)] == sys.ps1:
652 trimmed, lines[0] = self.trim(lines[0])
653 self.text.tag_add(
654 "compiled", "%d.%d" % (line, trimmed), "%d.0" % (line+1))
655 else:
656 self.text.tag_add("compiled", "%d.0" % line, "%d.0" % (line+1))
657
658 source = string.join(lines, "\n")
659 if not source:
660 self.continuation = 0
661 return
662
663 status, code = self.compile(source)
664
665 if status == "more":
666 self.text.tag_configure("compiled", background=self["morecolour"])
667 self.continuation = 1
668
669 elif status == "bad":
670 self.text.tag_configure("compiled", background=self["badcolour"])
671 self.error = 1
672 self.continuation = 0
673 self.intraceback = 1
674 oldout, olderr = sys.stdout, sys.stderr
675 sys.stdout, sys.stderr = self.stdout, self.stderr
676 traceback.print_exception(SyntaxError, code, None)
677 self.stdout, self.stderr = sys.stdout, sys.stderr
678 sys.stdout, sys.stderr = oldout, olderr
679 self.intraceback = 0
680
681 elif status == "okay":
682 if self.getline(lastline) == sys.ps2:
683 self.text.tag_remove("compiled", "%d.0" % lastline, "end")
684 self.text.tag_configure("compiled", background=self["runcolour"])
685 self.continuation = 0
686 self.run(code)
687
689 """Try to compile a piece of source code, returning a status code
690 and the compiled result. If the status code is "okay" the code is
691 complete and compiled successfully; if it is "more" then the code
692 can be compiled, but an interactive session should wait for more
693 input; if it is "bad" then there is a syntax error in the code and
694 the second returned value is the error message."""
695 err = err1 = err2 = None
696 code = code1 = code2 = None
697
698 try:
699 code = compile(source, "<console>", "single")
700 except SyntaxError, err:
701 pass
702 else:
703 return "okay", code
704
705 try:
706 code1 = compile(source + "\n", "<console>", "single")
707 except SyntaxError, err1:
708 pass
709 else:
710 return "more", code1
711
712 try:
713 code2 = compile(source + "\n\n", "<console>", "single")
714 except SyntaxError, err2:
715 pass
716
717 try:
718 code3 = compile(source + "\n", "<console>", "exec")
719 except SyntaxError, err3:
720 pass
721 else:
722 return "okay", code3
723
724 try:
725 code4 = compile(source + "\n\n", "<console>", "exec")
726 except SyntaxError, err4:
727 pass
728
729 if err3[1][2] != err4[1][2]:
730 return "more", None
731
732 if err1[1][2] != err2[1][2]:
733 return "more", None
734
735 return "bad", err1
736
737 - def run(self, code):
738 """Run a code object within the sandbox for this console. The
739 sandbox redirects stdout and stderr to the console, and executes
740 within the namespace associated with the console."""
741 oldout, olderr = sys.stdout, sys.stderr
742 sys.stdout, sys.stderr = self.stdout, self.stderr
743
744 try:
745 exec code in self.dict
746 except:
747 self.error = 1
748 sys.last_type = sys.exc_type
749 sys.last_value = sys.exc_value
750 sys.last_traceback = sys.exc_traceback.tb_next
751 self.intraceback = 1
752 traceback.print_exception(
753 sys.last_type, sys.last_value, sys.last_traceback)
754 self.intraceback = 0
755
756 self.stdout, self.stderr = sys.stdout, sys.stderr
757 sys.stdout, sys.stderr = oldout, olderr
758
759
760
761
763 for key in klass.__dict__.keys(): result[key] = 1
764 for base in klass.__bases__: scanclass(base, result)
765
767 result = {}
768 try:
769 for key in object.__members__: result[key] = 1
770 result["__members__"] = 1
771 except: pass
772 try:
773 for key in object.__methods__: result[key] = 1
774 result["__methods__"] = 1
775 except: pass
776 try:
777 for key in object.__dict__.keys(): result[key] = 1
778 result["__dict__"] = 1
779 except: pass
780 if type(object) is types.ClassType:
781 scanclass(object, result)
782 result["__name__"] = 1
783 result["__bases__"] = 1
784 if type(object) is types.InstanceType:
785 scanclass(object.__class__, result)
786 result["__class__"] = 1
787 return result.keys()
788
790 prefixmatch = lambda key, l=len(prefix), p=prefix: key[:l] == p
791 return filter(prefixmatch, keys)
792
794 if not keys: return ''
795 max = len(keys[0])
796 prefixes = map(lambda i, key=keys[0]: key[:i], range(max+1))
797 for key in keys:
798 while key[:max] != prefixes[max]:
799 max = max - 1
800 if max == 0: return ''
801 return prefixes[max]
802
803 callabletypes = [types.FunctionType, types.MethodType, types.ClassType,
804 types.BuiltinFunctionType, types.BuiltinMethodType]
805 sequencetypes = [types.TupleType, types.ListType]
806 mappingtypes = [types.DictType]
807
808 try:
809 import ExtensionClass
810 callabletypes.append(ExtensionClass.ExtensionClassType)
811 except: pass
812 try:
813 import curve
814 c = curve.Curve()
815 callabletypes.append(type(c.read))
816 except: pass
817
828
829
830
831
832 if __name__ == "__main__":
833 c = Console(dict={})
834 c.dict["console"] = c
835 c.pack(fill=BOTH, expand=1)
836 c.master.title("Python Console v%s" % VERSION)
837 mainloop()
838