• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#! /usr/bin/env python3
2
3import sys
4if __name__ == "__main__":
5    sys.modules['idlelib.pyshell'] = sys.modules['__main__']
6
7try:
8    from tkinter import *
9except ImportError:
10    print("** IDLE can't import Tkinter.\n"
11          "Your Python may not be configured for Tk. **", file=sys.__stderr__)
12    raise SystemExit(1)
13
14if sys.platform == 'win32':
15    from idlelib.util import fix_win_hidpi
16    fix_win_hidpi()
17
18from tkinter import messagebox
19
20from code import InteractiveInterpreter
21import itertools
22import linecache
23import os
24import os.path
25from platform import python_version
26import re
27import socket
28import subprocess
29from textwrap import TextWrapper
30import threading
31import time
32import tokenize
33import warnings
34
35from idlelib.colorizer import ColorDelegator
36from idlelib.config import idleConf
37from idlelib.delegator import Delegator
38from idlelib import debugger
39from idlelib import debugger_r
40from idlelib.editor import EditorWindow, fixwordbreaks
41from idlelib.filelist import FileList
42from idlelib.outwin import OutputWindow
43from idlelib import replace
44from idlelib import rpc
45from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile
46from idlelib.undo import UndoDelegator
47
48# Default for testing; defaults to True in main() for running.
49use_subprocess = False
50
51HOST = '127.0.0.1' # python execution server on localhost loopback
52PORT = 0  # someday pass in host, port for remote debug capability
53
54try:  # In case IDLE started with -n.
55    eof = 'Ctrl-D (end-of-file)'
56    exit.eof = eof
57    quit.eof = eof
58except NameError: # In case python started with -S.
59    pass
60
61# Override warnings module to write to warning_stream.  Initialize to send IDLE
62# internal warnings to the console.  ScriptBinding.check_syntax() will
63# temporarily redirect the stream to the shell window to display warnings when
64# checking user's code.
65warning_stream = sys.__stderr__  # None, at least on Windows, if no console.
66
67def idle_showwarning(
68        message, category, filename, lineno, file=None, line=None):
69    """Show Idle-format warning (after replacing warnings.showwarning).
70
71    The differences are the formatter called, the file=None replacement,
72    which can be None, the capture of the consequence AttributeError,
73    and the output of a hard-coded prompt.
74    """
75    if file is None:
76        file = warning_stream
77    try:
78        file.write(idle_formatwarning(
79                message, category, filename, lineno, line=line))
80        file.write(">>> ")
81    except (AttributeError, OSError):
82        pass  # if file (probably __stderr__) is invalid, skip warning.
83
84_warnings_showwarning = None
85
86def capture_warnings(capture):
87    "Replace warning.showwarning with idle_showwarning, or reverse."
88
89    global _warnings_showwarning
90    if capture:
91        if _warnings_showwarning is None:
92            _warnings_showwarning = warnings.showwarning
93            warnings.showwarning = idle_showwarning
94    else:
95        if _warnings_showwarning is not None:
96            warnings.showwarning = _warnings_showwarning
97            _warnings_showwarning = None
98
99capture_warnings(True)
100
101def extended_linecache_checkcache(filename=None,
102                                  orig_checkcache=linecache.checkcache):
103    """Extend linecache.checkcache to preserve the <pyshell#...> entries
104
105    Rather than repeating the linecache code, patch it to save the
106    <pyshell#...> entries, call the original linecache.checkcache()
107    (skipping them), and then restore the saved entries.
108
109    orig_checkcache is bound at definition time to the original
110    method, allowing it to be patched.
111    """
112    cache = linecache.cache
113    save = {}
114    for key in list(cache):
115        if key[:1] + key[-1:] == '<>':
116            save[key] = cache.pop(key)
117    orig_checkcache(filename)
118    cache.update(save)
119
120# Patch linecache.checkcache():
121linecache.checkcache = extended_linecache_checkcache
122
123
124class PyShellEditorWindow(EditorWindow):
125    "Regular text edit window in IDLE, supports breakpoints"
126
127    def __init__(self, *args):
128        self.breakpoints = []
129        EditorWindow.__init__(self, *args)
130        self.text.bind("<<set-breakpoint>>", self.set_breakpoint_event)
131        self.text.bind("<<clear-breakpoint>>", self.clear_breakpoint_event)
132        self.text.bind("<<open-python-shell>>", self.flist.open_shell)
133
134        #TODO: don't read/write this from/to .idlerc when testing
135        self.breakpointPath = os.path.join(
136                idleConf.userdir, 'breakpoints.lst')
137        # whenever a file is changed, restore breakpoints
138        def filename_changed_hook(old_hook=self.io.filename_change_hook,
139                                  self=self):
140            self.restore_file_breaks()
141            old_hook()
142        self.io.set_filename_change_hook(filename_changed_hook)
143        if self.io.filename:
144            self.restore_file_breaks()
145        self.color_breakpoint_text()
146
147    rmenu_specs = [
148        ("Cut", "<<cut>>", "rmenu_check_cut"),
149        ("Copy", "<<copy>>", "rmenu_check_copy"),
150        ("Paste", "<<paste>>", "rmenu_check_paste"),
151        (None, None, None),
152        ("Set Breakpoint", "<<set-breakpoint>>", None),
153        ("Clear Breakpoint", "<<clear-breakpoint>>", None)
154    ]
155
156    def color_breakpoint_text(self, color=True):
157        "Turn colorizing of breakpoint text on or off"
158        if self.io is None:
159            # possible due to update in restore_file_breaks
160            return
161        if color:
162            theme = idleConf.CurrentTheme()
163            cfg = idleConf.GetHighlight(theme, "break")
164        else:
165            cfg = {'foreground': '', 'background': ''}
166        self.text.tag_config('BREAK', cfg)
167
168    def set_breakpoint(self, lineno):
169        text = self.text
170        filename = self.io.filename
171        text.tag_add("BREAK", "%d.0" % lineno, "%d.0" % (lineno+1))
172        try:
173            self.breakpoints.index(lineno)
174        except ValueError:  # only add if missing, i.e. do once
175            self.breakpoints.append(lineno)
176        try:    # update the subprocess debugger
177            debug = self.flist.pyshell.interp.debugger
178            debug.set_breakpoint(filename, lineno)
179        except: # but debugger may not be active right now....
180            pass
181
182    def set_breakpoint_event(self, event=None):
183        text = self.text
184        filename = self.io.filename
185        if not filename:
186            text.bell()
187            return
188        lineno = int(float(text.index("insert")))
189        self.set_breakpoint(lineno)
190
191    def clear_breakpoint_event(self, event=None):
192        text = self.text
193        filename = self.io.filename
194        if not filename:
195            text.bell()
196            return
197        lineno = int(float(text.index("insert")))
198        try:
199            self.breakpoints.remove(lineno)
200        except:
201            pass
202        text.tag_remove("BREAK", "insert linestart",\
203                        "insert lineend +1char")
204        try:
205            debug = self.flist.pyshell.interp.debugger
206            debug.clear_breakpoint(filename, lineno)
207        except:
208            pass
209
210    def clear_file_breaks(self):
211        if self.breakpoints:
212            text = self.text
213            filename = self.io.filename
214            if not filename:
215                text.bell()
216                return
217            self.breakpoints = []
218            text.tag_remove("BREAK", "1.0", END)
219            try:
220                debug = self.flist.pyshell.interp.debugger
221                debug.clear_file_breaks(filename)
222            except:
223                pass
224
225    def store_file_breaks(self):
226        "Save breakpoints when file is saved"
227        # XXX 13 Dec 2002 KBK Currently the file must be saved before it can
228        #     be run.  The breaks are saved at that time.  If we introduce
229        #     a temporary file save feature the save breaks functionality
230        #     needs to be re-verified, since the breaks at the time the
231        #     temp file is created may differ from the breaks at the last
232        #     permanent save of the file.  Currently, a break introduced
233        #     after a save will be effective, but not persistent.
234        #     This is necessary to keep the saved breaks synched with the
235        #     saved file.
236        #
237        #     Breakpoints are set as tagged ranges in the text.
238        #     Since a modified file has to be saved before it is
239        #     run, and since self.breakpoints (from which the subprocess
240        #     debugger is loaded) is updated during the save, the visible
241        #     breaks stay synched with the subprocess even if one of these
242        #     unexpected breakpoint deletions occurs.
243        breaks = self.breakpoints
244        filename = self.io.filename
245        try:
246            with open(self.breakpointPath) as fp:
247                lines = fp.readlines()
248        except OSError:
249            lines = []
250        try:
251            with open(self.breakpointPath, "w") as new_file:
252                for line in lines:
253                    if not line.startswith(filename + '='):
254                        new_file.write(line)
255                self.update_breakpoints()
256                breaks = self.breakpoints
257                if breaks:
258                    new_file.write(filename + '=' + str(breaks) + '\n')
259        except OSError as err:
260            if not getattr(self.root, "breakpoint_error_displayed", False):
261                self.root.breakpoint_error_displayed = True
262                messagebox.showerror(title='IDLE Error',
263                    message='Unable to update breakpoint list:\n%s'
264                        % str(err),
265                    parent=self.text)
266
267    def restore_file_breaks(self):
268        self.text.update()   # this enables setting "BREAK" tags to be visible
269        if self.io is None:
270            # can happen if IDLE closes due to the .update() call
271            return
272        filename = self.io.filename
273        if filename is None:
274            return
275        if os.path.isfile(self.breakpointPath):
276            with open(self.breakpointPath) as fp:
277                lines = fp.readlines()
278            for line in lines:
279                if line.startswith(filename + '='):
280                    breakpoint_linenumbers = eval(line[len(filename)+1:])
281                    for breakpoint_linenumber in breakpoint_linenumbers:
282                        self.set_breakpoint(breakpoint_linenumber)
283
284    def update_breakpoints(self):
285        "Retrieves all the breakpoints in the current window"
286        text = self.text
287        ranges = text.tag_ranges("BREAK")
288        linenumber_list = self.ranges_to_linenumbers(ranges)
289        self.breakpoints = linenumber_list
290
291    def ranges_to_linenumbers(self, ranges):
292        lines = []
293        for index in range(0, len(ranges), 2):
294            lineno = int(float(ranges[index].string))
295            end = int(float(ranges[index+1].string))
296            while lineno < end:
297                lines.append(lineno)
298                lineno += 1
299        return lines
300
301# XXX 13 Dec 2002 KBK Not used currently
302#    def saved_change_hook(self):
303#        "Extend base method - clear breaks if module is modified"
304#        if not self.get_saved():
305#            self.clear_file_breaks()
306#        EditorWindow.saved_change_hook(self)
307
308    def _close(self):
309        "Extend base method - clear breaks when module is closed"
310        self.clear_file_breaks()
311        EditorWindow._close(self)
312
313
314class PyShellFileList(FileList):
315    "Extend base class: IDLE supports a shell and breakpoints"
316
317    # override FileList's class variable, instances return PyShellEditorWindow
318    # instead of EditorWindow when new edit windows are created.
319    EditorWindow = PyShellEditorWindow
320
321    pyshell = None
322
323    def open_shell(self, event=None):
324        if self.pyshell:
325            self.pyshell.top.wakeup()
326        else:
327            self.pyshell = PyShell(self)
328            if self.pyshell:
329                if not self.pyshell.begin():
330                    return None
331        return self.pyshell
332
333
334class ModifiedColorDelegator(ColorDelegator):
335    "Extend base class: colorizer for the shell window itself"
336    def recolorize_main(self):
337        self.tag_remove("TODO", "1.0", "iomark")
338        self.tag_add("SYNC", "1.0", "iomark")
339        ColorDelegator.recolorize_main(self)
340
341    def removecolors(self):
342        # Don't remove shell color tags before "iomark"
343        for tag in self.tagdefs:
344            self.tag_remove(tag, "iomark", "end")
345
346
347class ModifiedUndoDelegator(UndoDelegator):
348    "Extend base class: forbid insert/delete before the I/O mark"
349    def insert(self, index, chars, tags=None):
350        try:
351            if self.delegate.compare(index, "<", "iomark"):
352                self.delegate.bell()
353                return
354        except TclError:
355            pass
356        UndoDelegator.insert(self, index, chars, tags)
357
358    def delete(self, index1, index2=None):
359        try:
360            if self.delegate.compare(index1, "<", "iomark"):
361                self.delegate.bell()
362                return
363        except TclError:
364            pass
365        UndoDelegator.delete(self, index1, index2)
366
367    def undo_event(self, event):
368        # Temporarily monkey-patch the delegate's .insert() method to
369        # always use the "stdin" tag.  This is needed for undo-ing
370        # deletions to preserve the "stdin" tag, because UndoDelegator
371        # doesn't preserve tags for deleted text.
372        orig_insert = self.delegate.insert
373        self.delegate.insert = \
374            lambda index, chars: orig_insert(index, chars, "stdin")
375        try:
376            super().undo_event(event)
377        finally:
378            self.delegate.insert = orig_insert
379
380
381class UserInputTaggingDelegator(Delegator):
382    """Delegator used to tag user input with "stdin"."""
383    def insert(self, index, chars, tags=None):
384        if tags is None:
385            tags = "stdin"
386        self.delegate.insert(index, chars, tags)
387
388
389class MyRPCClient(rpc.RPCClient):
390
391    def handle_EOF(self):
392        "Override the base class - just re-raise EOFError"
393        raise EOFError
394
395def restart_line(width, filename):  # See bpo-38141.
396    """Return width long restart line formatted with filename.
397
398    Fill line with balanced '='s, with any extras and at least one at
399    the beginning.  Do not end with a trailing space.
400    """
401    tag = f"= RESTART: {filename or 'Shell'} ="
402    if width >= len(tag):
403        div, mod = divmod((width -len(tag)), 2)
404        return f"{(div+mod)*'='}{tag}{div*'='}"
405    else:
406        return tag[:-2]  # Remove ' ='.
407
408
409class ModifiedInterpreter(InteractiveInterpreter):
410
411    def __init__(self, tkconsole):
412        self.tkconsole = tkconsole
413        locals = sys.modules['__main__'].__dict__
414        InteractiveInterpreter.__init__(self, locals=locals)
415        self.restarting = False
416        self.subprocess_arglist = None
417        self.port = PORT
418        self.original_compiler_flags = self.compile.compiler.flags
419
420    _afterid = None
421    rpcclt = None
422    rpcsubproc = None
423
424    def spawn_subprocess(self):
425        if self.subprocess_arglist is None:
426            self.subprocess_arglist = self.build_subprocess_arglist()
427        self.rpcsubproc = subprocess.Popen(self.subprocess_arglist)
428
429    def build_subprocess_arglist(self):
430        assert (self.port!=0), (
431            "Socket should have been assigned a port number.")
432        w = ['-W' + s for s in sys.warnoptions]
433        # Maybe IDLE is installed and is being accessed via sys.path,
434        # or maybe it's not installed and the idle.py script is being
435        # run from the IDLE source directory.
436        del_exitf = idleConf.GetOption('main', 'General', 'delete-exitfunc',
437                                       default=False, type='bool')
438        command = f"__import__('idlelib.run').run.main({del_exitf!r})"
439        return [sys.executable] + w + ["-c", command, str(self.port)]
440
441    def start_subprocess(self):
442        addr = (HOST, self.port)
443        # GUI makes several attempts to acquire socket, listens for connection
444        for i in range(3):
445            time.sleep(i)
446            try:
447                self.rpcclt = MyRPCClient(addr)
448                break
449            except OSError:
450                pass
451        else:
452            self.display_port_binding_error()
453            return None
454        # if PORT was 0, system will assign an 'ephemeral' port. Find it out:
455        self.port = self.rpcclt.listening_sock.getsockname()[1]
456        # if PORT was not 0, probably working with a remote execution server
457        if PORT != 0:
458            # To allow reconnection within the 2MSL wait (cf. Stevens TCP
459            # V1, 18.6),  set SO_REUSEADDR.  Note that this can be problematic
460            # on Windows since the implementation allows two active sockets on
461            # the same address!
462            self.rpcclt.listening_sock.setsockopt(socket.SOL_SOCKET,
463                                           socket.SO_REUSEADDR, 1)
464        self.spawn_subprocess()
465        #time.sleep(20) # test to simulate GUI not accepting connection
466        # Accept the connection from the Python execution server
467        self.rpcclt.listening_sock.settimeout(10)
468        try:
469            self.rpcclt.accept()
470        except TimeoutError:
471            self.display_no_subprocess_error()
472            return None
473        self.rpcclt.register("console", self.tkconsole)
474        self.rpcclt.register("stdin", self.tkconsole.stdin)
475        self.rpcclt.register("stdout", self.tkconsole.stdout)
476        self.rpcclt.register("stderr", self.tkconsole.stderr)
477        self.rpcclt.register("flist", self.tkconsole.flist)
478        self.rpcclt.register("linecache", linecache)
479        self.rpcclt.register("interp", self)
480        self.transfer_path(with_cwd=True)
481        self.poll_subprocess()
482        return self.rpcclt
483
484    def restart_subprocess(self, with_cwd=False, filename=''):
485        if self.restarting:
486            return self.rpcclt
487        self.restarting = True
488        # close only the subprocess debugger
489        debug = self.getdebugger()
490        if debug:
491            try:
492                # Only close subprocess debugger, don't unregister gui_adap!
493                debugger_r.close_subprocess_debugger(self.rpcclt)
494            except:
495                pass
496        # Kill subprocess, spawn a new one, accept connection.
497        self.rpcclt.close()
498        self.terminate_subprocess()
499        console = self.tkconsole
500        was_executing = console.executing
501        console.executing = False
502        self.spawn_subprocess()
503        try:
504            self.rpcclt.accept()
505        except TimeoutError:
506            self.display_no_subprocess_error()
507            return None
508        self.transfer_path(with_cwd=with_cwd)
509        console.stop_readline()
510        # annotate restart in shell window and mark it
511        console.text.delete("iomark", "end-1c")
512        console.write('\n')
513        console.write(restart_line(console.width, filename))
514        console.text.mark_set("restart", "end-1c")
515        console.text.mark_gravity("restart", "left")
516        if not filename:
517            console.showprompt()
518        # restart subprocess debugger
519        if debug:
520            # Restarted debugger connects to current instance of debug GUI
521            debugger_r.restart_subprocess_debugger(self.rpcclt)
522            # reload remote debugger breakpoints for all PyShellEditWindows
523            debug.load_breakpoints()
524        self.compile.compiler.flags = self.original_compiler_flags
525        self.restarting = False
526        return self.rpcclt
527
528    def __request_interrupt(self):
529        self.rpcclt.remotecall("exec", "interrupt_the_server", (), {})
530
531    def interrupt_subprocess(self):
532        threading.Thread(target=self.__request_interrupt).start()
533
534    def kill_subprocess(self):
535        if self._afterid is not None:
536            self.tkconsole.text.after_cancel(self._afterid)
537        try:
538            self.rpcclt.listening_sock.close()
539        except AttributeError:  # no socket
540            pass
541        try:
542            self.rpcclt.close()
543        except AttributeError:  # no socket
544            pass
545        self.terminate_subprocess()
546        self.tkconsole.executing = False
547        self.rpcclt = None
548
549    def terminate_subprocess(self):
550        "Make sure subprocess is terminated"
551        try:
552            self.rpcsubproc.kill()
553        except OSError:
554            # process already terminated
555            return
556        else:
557            try:
558                self.rpcsubproc.wait()
559            except OSError:
560                return
561
562    def transfer_path(self, with_cwd=False):
563        if with_cwd:        # Issue 13506
564            path = ['']     # include Current Working Directory
565            path.extend(sys.path)
566        else:
567            path = sys.path
568
569        self.runcommand("""if 1:
570        import sys as _sys
571        _sys.path = {!r}
572        del _sys
573        \n""".format(path))
574
575    active_seq = None
576
577    def poll_subprocess(self):
578        clt = self.rpcclt
579        if clt is None:
580            return
581        try:
582            response = clt.pollresponse(self.active_seq, wait=0.05)
583        except (EOFError, OSError, KeyboardInterrupt):
584            # lost connection or subprocess terminated itself, restart
585            # [the KBI is from rpc.SocketIO.handle_EOF()]
586            if self.tkconsole.closing:
587                return
588            response = None
589            self.restart_subprocess()
590        if response:
591            self.tkconsole.resetoutput()
592            self.active_seq = None
593            how, what = response
594            console = self.tkconsole.console
595            if how == "OK":
596                if what is not None:
597                    print(repr(what), file=console)
598            elif how == "EXCEPTION":
599                if self.tkconsole.getvar("<<toggle-jit-stack-viewer>>"):
600                    self.remote_stack_viewer()
601            elif how == "ERROR":
602                errmsg = "pyshell.ModifiedInterpreter: Subprocess ERROR:\n"
603                print(errmsg, what, file=sys.__stderr__)
604                print(errmsg, what, file=console)
605            # we received a response to the currently active seq number:
606            try:
607                self.tkconsole.endexecuting()
608            except AttributeError:  # shell may have closed
609                pass
610        # Reschedule myself
611        if not self.tkconsole.closing:
612            self._afterid = self.tkconsole.text.after(
613                self.tkconsole.pollinterval, self.poll_subprocess)
614
615    debugger = None
616
617    def setdebugger(self, debugger):
618        self.debugger = debugger
619
620    def getdebugger(self):
621        return self.debugger
622
623    def open_remote_stack_viewer(self):
624        """Initiate the remote stack viewer from a separate thread.
625
626        This method is called from the subprocess, and by returning from this
627        method we allow the subprocess to unblock.  After a bit the shell
628        requests the subprocess to open the remote stack viewer which returns a
629        static object looking at the last exception.  It is queried through
630        the RPC mechanism.
631
632        """
633        self.tkconsole.text.after(300, self.remote_stack_viewer)
634        return
635
636    def remote_stack_viewer(self):
637        from idlelib import debugobj_r
638        oid = self.rpcclt.remotequeue("exec", "stackviewer", ("flist",), {})
639        if oid is None:
640            self.tkconsole.root.bell()
641            return
642        item = debugobj_r.StubObjectTreeItem(self.rpcclt, oid)
643        from idlelib.tree import ScrolledCanvas, TreeNode
644        top = Toplevel(self.tkconsole.root)
645        theme = idleConf.CurrentTheme()
646        background = idleConf.GetHighlight(theme, 'normal')['background']
647        sc = ScrolledCanvas(top, bg=background, highlightthickness=0)
648        sc.frame.pack(expand=1, fill="both")
649        node = TreeNode(sc.canvas, None, item)
650        node.expand()
651        # XXX Should GC the remote tree when closing the window
652
653    gid = 0
654
655    def execsource(self, source):
656        "Like runsource() but assumes complete exec source"
657        filename = self.stuffsource(source)
658        self.execfile(filename, source)
659
660    def execfile(self, filename, source=None):
661        "Execute an existing file"
662        if source is None:
663            with tokenize.open(filename) as fp:
664                source = fp.read()
665                if use_subprocess:
666                    source = (f"__file__ = r'''{os.path.abspath(filename)}'''\n"
667                              + source + "\ndel __file__")
668        try:
669            code = compile(source, filename, "exec")
670        except (OverflowError, SyntaxError):
671            self.tkconsole.resetoutput()
672            print('*** Error in script or command!\n'
673                 'Traceback (most recent call last):',
674                  file=self.tkconsole.stderr)
675            InteractiveInterpreter.showsyntaxerror(self, filename)
676            self.tkconsole.showprompt()
677        else:
678            self.runcode(code)
679
680    def runsource(self, source):
681        "Extend base class method: Stuff the source in the line cache first"
682        filename = self.stuffsource(source)
683        # at the moment, InteractiveInterpreter expects str
684        assert isinstance(source, str)
685        # InteractiveInterpreter.runsource() calls its runcode() method,
686        # which is overridden (see below)
687        return InteractiveInterpreter.runsource(self, source, filename)
688
689    def stuffsource(self, source):
690        "Stuff source in the filename cache"
691        filename = "<pyshell#%d>" % self.gid
692        self.gid = self.gid + 1
693        lines = source.split("\n")
694        linecache.cache[filename] = len(source)+1, 0, lines, filename
695        return filename
696
697    def prepend_syspath(self, filename):
698        "Prepend sys.path with file's directory if not already included"
699        self.runcommand("""if 1:
700            _filename = {!r}
701            import sys as _sys
702            from os.path import dirname as _dirname
703            _dir = _dirname(_filename)
704            if not _dir in _sys.path:
705                _sys.path.insert(0, _dir)
706            del _filename, _sys, _dirname, _dir
707            \n""".format(filename))
708
709    def showsyntaxerror(self, filename=None, **kwargs):
710        """Override Interactive Interpreter method: Use Colorizing
711
712        Color the offending position instead of printing it and pointing at it
713        with a caret.
714
715        """
716        tkconsole = self.tkconsole
717        text = tkconsole.text
718        text.tag_remove("ERROR", "1.0", "end")
719        type, value, tb = sys.exc_info()
720        msg = getattr(value, 'msg', '') or value or "<no detail available>"
721        lineno = getattr(value, 'lineno', '') or 1
722        offset = getattr(value, 'offset', '') or 0
723        if offset == 0:
724            lineno += 1 #mark end of offending line
725        if lineno == 1:
726            pos = "iomark + %d chars" % (offset-1)
727        else:
728            pos = "iomark linestart + %d lines + %d chars" % \
729                  (lineno-1, offset-1)
730        tkconsole.colorize_syntax_error(text, pos)
731        tkconsole.resetoutput()
732        self.write("SyntaxError: %s\n" % msg)
733        tkconsole.showprompt()
734
735    def showtraceback(self):
736        "Extend base class method to reset output properly"
737        self.tkconsole.resetoutput()
738        self.checklinecache()
739        InteractiveInterpreter.showtraceback(self)
740        if self.tkconsole.getvar("<<toggle-jit-stack-viewer>>"):
741            self.tkconsole.open_stack_viewer()
742
743    def checklinecache(self):
744        "Remove keys other than '<pyshell#n>'."
745        cache = linecache.cache
746        for key in list(cache):  # Iterate list because mutate cache.
747            if key[:1] + key[-1:] != "<>":
748                del cache[key]
749
750    def runcommand(self, code):
751        "Run the code without invoking the debugger"
752        # The code better not raise an exception!
753        if self.tkconsole.executing:
754            self.display_executing_dialog()
755            return 0
756        if self.rpcclt:
757            self.rpcclt.remotequeue("exec", "runcode", (code,), {})
758        else:
759            exec(code, self.locals)
760        return 1
761
762    def runcode(self, code):
763        "Override base class method"
764        if self.tkconsole.executing:
765            self.restart_subprocess()
766        self.checklinecache()
767        debugger = self.debugger
768        try:
769            self.tkconsole.beginexecuting()
770            if not debugger and self.rpcclt is not None:
771                self.active_seq = self.rpcclt.asyncqueue("exec", "runcode",
772                                                        (code,), {})
773            elif debugger:
774                debugger.run(code, self.locals)
775            else:
776                exec(code, self.locals)
777        except SystemExit:
778            if not self.tkconsole.closing:
779                if messagebox.askyesno(
780                    "Exit?",
781                    "Do you want to exit altogether?",
782                    default="yes",
783                    parent=self.tkconsole.text):
784                    raise
785                else:
786                    self.showtraceback()
787            else:
788                raise
789        except:
790            if use_subprocess:
791                print("IDLE internal error in runcode()",
792                      file=self.tkconsole.stderr)
793                self.showtraceback()
794                self.tkconsole.endexecuting()
795            else:
796                if self.tkconsole.canceled:
797                    self.tkconsole.canceled = False
798                    print("KeyboardInterrupt", file=self.tkconsole.stderr)
799                else:
800                    self.showtraceback()
801        finally:
802            if not use_subprocess:
803                try:
804                    self.tkconsole.endexecuting()
805                except AttributeError:  # shell may have closed
806                    pass
807
808    def write(self, s):
809        "Override base class method"
810        return self.tkconsole.stderr.write(s)
811
812    def display_port_binding_error(self):
813        messagebox.showerror(
814            "Port Binding Error",
815            "IDLE can't bind to a TCP/IP port, which is necessary to "
816            "communicate with its Python execution server.  This might be "
817            "because no networking is installed on this computer.  "
818            "Run IDLE with the -n command line switch to start without a "
819            "subprocess and refer to Help/IDLE Help 'Running without a "
820            "subprocess' for further details.",
821            parent=self.tkconsole.text)
822
823    def display_no_subprocess_error(self):
824        messagebox.showerror(
825            "Subprocess Connection Error",
826            "IDLE's subprocess didn't make connection.\n"
827            "See the 'Startup failure' section of the IDLE doc, online at\n"
828            "https://docs.python.org/3/library/idle.html#startup-failure",
829            parent=self.tkconsole.text)
830
831    def display_executing_dialog(self):
832        messagebox.showerror(
833            "Already executing",
834            "The Python Shell window is already executing a command; "
835            "please wait until it is finished.",
836            parent=self.tkconsole.text)
837
838
839class PyShell(OutputWindow):
840    from idlelib.squeezer import Squeezer
841
842    shell_title = "IDLE Shell " + python_version()
843
844    # Override classes
845    ColorDelegator = ModifiedColorDelegator
846    UndoDelegator = ModifiedUndoDelegator
847
848    # Override menus
849    menu_specs = [
850        ("file", "_File"),
851        ("edit", "_Edit"),
852        ("debug", "_Debug"),
853        ("options", "_Options"),
854        ("window", "_Window"),
855        ("help", "_Help"),
856    ]
857
858    # Extend right-click context menu
859    rmenu_specs = OutputWindow.rmenu_specs + [
860        ("Squeeze", "<<squeeze-current-text>>"),
861    ]
862    _idx = 1 + len(list(itertools.takewhile(
863        lambda rmenu_item: rmenu_item[0] != "Copy", rmenu_specs)
864    ))
865    rmenu_specs.insert(_idx, ("Copy with prompts",
866                              "<<copy-with-prompts>>",
867                              "rmenu_check_copy"))
868    del _idx
869
870    allow_line_numbers = False
871    user_input_insert_tags = "stdin"
872
873    # New classes
874    from idlelib.history import History
875    from idlelib.sidebar import ShellSidebar
876
877    def __init__(self, flist=None):
878        if use_subprocess:
879            ms = self.menu_specs
880            if ms[2][0] != "shell":
881                ms.insert(2, ("shell", "She_ll"))
882        self.interp = ModifiedInterpreter(self)
883        if flist is None:
884            root = Tk()
885            fixwordbreaks(root)
886            root.withdraw()
887            flist = PyShellFileList(root)
888
889        self.shell_sidebar = None  # initialized below
890
891        OutputWindow.__init__(self, flist, None, None)
892
893        self.usetabs = False
894        # indentwidth must be 8 when using tabs.  See note in EditorWindow:
895        self.indentwidth = 4
896
897        self.sys_ps1 = sys.ps1 if hasattr(sys, 'ps1') else '>>>\n'
898        self.prompt_last_line = self.sys_ps1.split('\n')[-1]
899        self.prompt = self.sys_ps1  # Changes when debug active
900
901        text = self.text
902        text.configure(wrap="char")
903        text.bind("<<newline-and-indent>>", self.enter_callback)
904        text.bind("<<plain-newline-and-indent>>", self.linefeed_callback)
905        text.bind("<<interrupt-execution>>", self.cancel_callback)
906        text.bind("<<end-of-file>>", self.eof_callback)
907        text.bind("<<open-stack-viewer>>", self.open_stack_viewer)
908        text.bind("<<toggle-debugger>>", self.toggle_debugger)
909        text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer)
910        text.bind("<<copy-with-prompts>>", self.copy_with_prompts_callback)
911        if use_subprocess:
912            text.bind("<<view-restart>>", self.view_restart_mark)
913            text.bind("<<restart-shell>>", self.restart_shell)
914        self.squeezer = self.Squeezer(self)
915        text.bind("<<squeeze-current-text>>",
916                  self.squeeze_current_text_event)
917
918        self.save_stdout = sys.stdout
919        self.save_stderr = sys.stderr
920        self.save_stdin = sys.stdin
921        from idlelib import iomenu
922        self.stdin = StdInputFile(self, "stdin",
923                                  iomenu.encoding, iomenu.errors)
924        self.stdout = StdOutputFile(self, "stdout",
925                                    iomenu.encoding, iomenu.errors)
926        self.stderr = StdOutputFile(self, "stderr",
927                                    iomenu.encoding, "backslashreplace")
928        self.console = StdOutputFile(self, "console",
929                                     iomenu.encoding, iomenu.errors)
930        if not use_subprocess:
931            sys.stdout = self.stdout
932            sys.stderr = self.stderr
933            sys.stdin = self.stdin
934        try:
935            # page help() text to shell.
936            import pydoc # import must be done here to capture i/o rebinding.
937            # XXX KBK 27Dec07 use text viewer someday, but must work w/o subproc
938            pydoc.pager = pydoc.plainpager
939        except:
940            sys.stderr = sys.__stderr__
941            raise
942        #
943        self.history = self.History(self.text)
944        #
945        self.pollinterval = 50  # millisec
946
947        self.shell_sidebar = self.ShellSidebar(self)
948
949        # Insert UserInputTaggingDelegator at the top of the percolator,
950        # but make calls to text.insert() skip it.  This causes only insert
951        # events generated in Tcl/Tk to go through this delegator.
952        self.text.insert = self.per.top.insert
953        self.per.insertfilter(UserInputTaggingDelegator())
954
955    def ResetFont(self):
956        super().ResetFont()
957
958        if self.shell_sidebar is not None:
959            self.shell_sidebar.update_font()
960
961    def ResetColorizer(self):
962        super().ResetColorizer()
963
964        theme = idleConf.CurrentTheme()
965        tag_colors = {
966          "stdin": {'background': None, 'foreground': None},
967          "stdout": idleConf.GetHighlight(theme, "stdout"),
968          "stderr": idleConf.GetHighlight(theme, "stderr"),
969          "console": idleConf.GetHighlight(theme, "normal"),
970        }
971        for tag, tag_colors_config in tag_colors.items():
972            self.text.tag_configure(tag, **tag_colors_config)
973
974        if self.shell_sidebar is not None:
975            self.shell_sidebar.update_colors()
976
977    def replace_event(self, event):
978        replace.replace(self.text, insert_tags="stdin")
979        return "break"
980
981    def get_standard_extension_names(self):
982        return idleConf.GetExtensions(shell_only=True)
983
984    def get_prompt_text(self, first, last):
985        """Return text between first and last with prompts added."""
986        text = self.text.get(first, last)
987        lineno_range = range(
988            int(float(first)),
989            int(float(last))
990         )
991        prompts = [
992            self.shell_sidebar.line_prompts.get(lineno)
993            for lineno in lineno_range
994        ]
995        return "\n".join(
996            line if prompt is None else f"{prompt} {line}"
997            for prompt, line in zip(prompts, text.splitlines())
998        ) + "\n"
999
1000
1001    def copy_with_prompts_callback(self, event=None):
1002        """Copy selected lines to the clipboard, with prompts.
1003
1004        This makes the copied text useful for doc-tests and interactive
1005        shell code examples.
1006
1007        This always copies entire lines, even if only part of the first
1008        and/or last lines is selected.
1009        """
1010        text = self.text
1011        selfirst = text.index('sel.first linestart')
1012        if selfirst is None:  # Should not be possible.
1013            return  # No selection, do nothing.
1014        sellast = text.index('sel.last')
1015        if sellast[-1] != '0':
1016            sellast = text.index("sel.last+1line linestart")
1017        text.clipboard_clear()
1018        prompt_text = self.get_prompt_text(selfirst, sellast)
1019        text.clipboard_append(prompt_text)
1020
1021    reading = False
1022    executing = False
1023    canceled = False
1024    endoffile = False
1025    closing = False
1026    _stop_readline_flag = False
1027
1028    def set_warning_stream(self, stream):
1029        global warning_stream
1030        warning_stream = stream
1031
1032    def get_warning_stream(self):
1033        return warning_stream
1034
1035    def toggle_debugger(self, event=None):
1036        if self.executing:
1037            messagebox.showerror("Don't debug now",
1038                "You can only toggle the debugger when idle",
1039                parent=self.text)
1040            self.set_debugger_indicator()
1041            return "break"
1042        else:
1043            db = self.interp.getdebugger()
1044            if db:
1045                self.close_debugger()
1046            else:
1047                self.open_debugger()
1048
1049    def set_debugger_indicator(self):
1050        db = self.interp.getdebugger()
1051        self.setvar("<<toggle-debugger>>", not not db)
1052
1053    def toggle_jit_stack_viewer(self, event=None):
1054        pass # All we need is the variable
1055
1056    def close_debugger(self):
1057        db = self.interp.getdebugger()
1058        if db:
1059            self.interp.setdebugger(None)
1060            db.close()
1061            if self.interp.rpcclt:
1062                debugger_r.close_remote_debugger(self.interp.rpcclt)
1063            self.resetoutput()
1064            self.console.write("[DEBUG OFF]\n")
1065            self.prompt = self.sys_ps1
1066            self.showprompt()
1067        self.set_debugger_indicator()
1068
1069    def open_debugger(self):
1070        if self.interp.rpcclt:
1071            dbg_gui = debugger_r.start_remote_debugger(self.interp.rpcclt,
1072                                                           self)
1073        else:
1074            dbg_gui = debugger.Debugger(self)
1075        self.interp.setdebugger(dbg_gui)
1076        dbg_gui.load_breakpoints()
1077        self.prompt = "[DEBUG ON]\n" + self.sys_ps1
1078        self.showprompt()
1079        self.set_debugger_indicator()
1080
1081    def debug_menu_postcommand(self):
1082        state = 'disabled' if self.executing else 'normal'
1083        self.update_menu_state('debug', '*tack*iewer', state)
1084
1085    def beginexecuting(self):
1086        "Helper for ModifiedInterpreter"
1087        self.resetoutput()
1088        self.executing = True
1089
1090    def endexecuting(self):
1091        "Helper for ModifiedInterpreter"
1092        self.executing = False
1093        self.canceled = False
1094        self.showprompt()
1095
1096    def close(self):
1097        "Extend EditorWindow.close()"
1098        if self.executing:
1099            response = messagebox.askokcancel(
1100                "Kill?",
1101                "Your program is still running!\n Do you want to kill it?",
1102                default="ok",
1103                parent=self.text)
1104            if response is False:
1105                return "cancel"
1106        self.stop_readline()
1107        self.canceled = True
1108        self.closing = True
1109        return EditorWindow.close(self)
1110
1111    def _close(self):
1112        "Extend EditorWindow._close(), shut down debugger and execution server"
1113        self.close_debugger()
1114        if use_subprocess:
1115            self.interp.kill_subprocess()
1116        # Restore std streams
1117        sys.stdout = self.save_stdout
1118        sys.stderr = self.save_stderr
1119        sys.stdin = self.save_stdin
1120        # Break cycles
1121        self.interp = None
1122        self.console = None
1123        self.flist.pyshell = None
1124        self.history = None
1125        EditorWindow._close(self)
1126
1127    def ispythonsource(self, filename):
1128        "Override EditorWindow method: never remove the colorizer"
1129        return True
1130
1131    def short_title(self):
1132        return self.shell_title
1133
1134    COPYRIGHT = \
1135          'Type "help", "copyright", "credits" or "license()" for more information.'
1136
1137    def begin(self):
1138        self.text.mark_set("iomark", "insert")
1139        self.resetoutput()
1140        if use_subprocess:
1141            nosub = ''
1142            client = self.interp.start_subprocess()
1143            if not client:
1144                self.close()
1145                return False
1146        else:
1147            nosub = ("==== No Subprocess ====\n\n" +
1148                    "WARNING: Running IDLE without a Subprocess is deprecated\n" +
1149                    "and will be removed in a later version. See Help/IDLE Help\n" +
1150                    "for details.\n\n")
1151            sys.displayhook = rpc.displayhook
1152
1153        self.write("Python %s on %s\n%s\n%s" %
1154                   (sys.version, sys.platform, self.COPYRIGHT, nosub))
1155        self.text.focus_force()
1156        self.showprompt()
1157        # User code should use separate default Tk root window
1158        import tkinter
1159        tkinter._support_default_root = True
1160        tkinter._default_root = None
1161        return True
1162
1163    def stop_readline(self):
1164        if not self.reading:  # no nested mainloop to exit.
1165            return
1166        self._stop_readline_flag = True
1167        self.top.quit()
1168
1169    def readline(self):
1170        save = self.reading
1171        try:
1172            self.reading = True
1173            self.top.mainloop()  # nested mainloop()
1174        finally:
1175            self.reading = save
1176        if self._stop_readline_flag:
1177            self._stop_readline_flag = False
1178            return ""
1179        line = self.text.get("iomark", "end-1c")
1180        if len(line) == 0:  # may be EOF if we quit our mainloop with Ctrl-C
1181            line = "\n"
1182        self.resetoutput()
1183        if self.canceled:
1184            self.canceled = False
1185            if not use_subprocess:
1186                raise KeyboardInterrupt
1187        if self.endoffile:
1188            self.endoffile = False
1189            line = ""
1190        return line
1191
1192    def isatty(self):
1193        return True
1194
1195    def cancel_callback(self, event=None):
1196        try:
1197            if self.text.compare("sel.first", "!=", "sel.last"):
1198                return # Active selection -- always use default binding
1199        except:
1200            pass
1201        if not (self.executing or self.reading):
1202            self.resetoutput()
1203            self.interp.write("KeyboardInterrupt\n")
1204            self.showprompt()
1205            return "break"
1206        self.endoffile = False
1207        self.canceled = True
1208        if (self.executing and self.interp.rpcclt):
1209            if self.interp.getdebugger():
1210                self.interp.restart_subprocess()
1211            else:
1212                self.interp.interrupt_subprocess()
1213        if self.reading:
1214            self.top.quit()  # exit the nested mainloop() in readline()
1215        return "break"
1216
1217    def eof_callback(self, event):
1218        if self.executing and not self.reading:
1219            return # Let the default binding (delete next char) take over
1220        if not (self.text.compare("iomark", "==", "insert") and
1221                self.text.compare("insert", "==", "end-1c")):
1222            return # Let the default binding (delete next char) take over
1223        if not self.executing:
1224            self.resetoutput()
1225            self.close()
1226        else:
1227            self.canceled = False
1228            self.endoffile = True
1229            self.top.quit()
1230        return "break"
1231
1232    def linefeed_callback(self, event):
1233        # Insert a linefeed without entering anything (still autoindented)
1234        if self.reading:
1235            self.text.insert("insert", "\n")
1236            self.text.see("insert")
1237        else:
1238            self.newline_and_indent_event(event)
1239        return "break"
1240
1241    def enter_callback(self, event):
1242        if self.executing and not self.reading:
1243            return # Let the default binding (insert '\n') take over
1244        # If some text is selected, recall the selection
1245        # (but only if this before the I/O mark)
1246        try:
1247            sel = self.text.get("sel.first", "sel.last")
1248            if sel:
1249                if self.text.compare("sel.last", "<=", "iomark"):
1250                    self.recall(sel, event)
1251                    return "break"
1252        except:
1253            pass
1254        # If we're strictly before the line containing iomark, recall
1255        # the current line, less a leading prompt, less leading or
1256        # trailing whitespace
1257        if self.text.compare("insert", "<", "iomark linestart"):
1258            # Check if there's a relevant stdin range -- if so, use it.
1259            # Note: "stdin" blocks may include several successive statements,
1260            # so look for "console" tags on the newline before each statement
1261            # (and possibly on prompts).
1262            prev = self.text.tag_prevrange("stdin", "insert")
1263            if (
1264                    prev and
1265                    self.text.compare("insert", "<", prev[1]) and
1266                    # The following is needed to handle empty statements.
1267                    "console" not in self.text.tag_names("insert")
1268            ):
1269                prev_cons = self.text.tag_prevrange("console", "insert")
1270                if prev_cons and self.text.compare(prev_cons[1], ">=", prev[0]):
1271                    prev = (prev_cons[1], prev[1])
1272                next_cons = self.text.tag_nextrange("console", "insert")
1273                if next_cons and self.text.compare(next_cons[0], "<", prev[1]):
1274                    prev = (prev[0], self.text.index(next_cons[0] + "+1c"))
1275                self.recall(self.text.get(prev[0], prev[1]), event)
1276                return "break"
1277            next = self.text.tag_nextrange("stdin", "insert")
1278            if next and self.text.compare("insert lineend", ">=", next[0]):
1279                next_cons = self.text.tag_nextrange("console", "insert lineend")
1280                if next_cons and self.text.compare(next_cons[0], "<", next[1]):
1281                    next = (next[0], self.text.index(next_cons[0] + "+1c"))
1282                self.recall(self.text.get(next[0], next[1]), event)
1283                return "break"
1284            # No stdin mark -- just get the current line, less any prompt
1285            indices = self.text.tag_nextrange("console", "insert linestart")
1286            if indices and \
1287               self.text.compare(indices[0], "<=", "insert linestart"):
1288                self.recall(self.text.get(indices[1], "insert lineend"), event)
1289            else:
1290                self.recall(self.text.get("insert linestart", "insert lineend"), event)
1291            return "break"
1292        # If we're between the beginning of the line and the iomark, i.e.
1293        # in the prompt area, move to the end of the prompt
1294        if self.text.compare("insert", "<", "iomark"):
1295            self.text.mark_set("insert", "iomark")
1296        # If we're in the current input and there's only whitespace
1297        # beyond the cursor, erase that whitespace first
1298        s = self.text.get("insert", "end-1c")
1299        if s and not s.strip():
1300            self.text.delete("insert", "end-1c")
1301        # If we're in the current input before its last line,
1302        # insert a newline right at the insert point
1303        if self.text.compare("insert", "<", "end-1c linestart"):
1304            self.newline_and_indent_event(event)
1305            return "break"
1306        # We're in the last line; append a newline and submit it
1307        self.text.mark_set("insert", "end-1c")
1308        if self.reading:
1309            self.text.insert("insert", "\n")
1310            self.text.see("insert")
1311        else:
1312            self.newline_and_indent_event(event)
1313        self.text.update_idletasks()
1314        if self.reading:
1315            self.top.quit() # Break out of recursive mainloop()
1316        else:
1317            self.runit()
1318        return "break"
1319
1320    def recall(self, s, event):
1321        # remove leading and trailing empty or whitespace lines
1322        s = re.sub(r'^\s*\n', '', s)
1323        s = re.sub(r'\n\s*$', '', s)
1324        lines = s.split('\n')
1325        self.text.undo_block_start()
1326        try:
1327            self.text.tag_remove("sel", "1.0", "end")
1328            self.text.mark_set("insert", "end-1c")
1329            prefix = self.text.get("insert linestart", "insert")
1330            if prefix.rstrip().endswith(':'):
1331                self.newline_and_indent_event(event)
1332                prefix = self.text.get("insert linestart", "insert")
1333            self.text.insert("insert", lines[0].strip(),
1334                             self.user_input_insert_tags)
1335            if len(lines) > 1:
1336                orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0)
1337                new_base_indent  = re.search(r'^([ \t]*)', prefix).group(0)
1338                for line in lines[1:]:
1339                    if line.startswith(orig_base_indent):
1340                        # replace orig base indentation with new indentation
1341                        line = new_base_indent + line[len(orig_base_indent):]
1342                    self.text.insert('insert', '\n' + line.rstrip(),
1343                                     self.user_input_insert_tags)
1344        finally:
1345            self.text.see("insert")
1346            self.text.undo_block_stop()
1347
1348    _last_newline_re = re.compile(r"[ \t]*(\n[ \t]*)?\Z")
1349    def runit(self):
1350        index_before = self.text.index("end-2c")
1351        line = self.text.get("iomark", "end-1c")
1352        # Strip off last newline and surrounding whitespace.
1353        # (To allow you to hit return twice to end a statement.)
1354        line = self._last_newline_re.sub("", line)
1355        input_is_complete = self.interp.runsource(line)
1356        if not input_is_complete:
1357            if self.text.get(index_before) == '\n':
1358                self.text.tag_remove(self.user_input_insert_tags, index_before)
1359            self.shell_sidebar.update_sidebar()
1360
1361    def open_stack_viewer(self, event=None):  # -n mode only
1362        if self.interp.rpcclt:
1363            return self.interp.remote_stack_viewer()
1364
1365        from idlelib.stackviewer import StackBrowser
1366        try:
1367            StackBrowser(self.root, sys.last_exc, self.flist)
1368        except:
1369            messagebox.showerror("No stack trace",
1370                "There is no stack trace yet.\n"
1371                "(sys.last_exc is not defined)",
1372                parent=self.text)
1373        return None
1374
1375    def view_restart_mark(self, event=None):
1376        self.text.see("iomark")
1377        self.text.see("restart")
1378
1379    def restart_shell(self, event=None):
1380        "Callback for Run/Restart Shell Cntl-F6"
1381        self.interp.restart_subprocess(with_cwd=True)
1382
1383    def showprompt(self):
1384        self.resetoutput()
1385
1386        prompt = self.prompt
1387        if self.sys_ps1 and prompt.endswith(self.sys_ps1):
1388            prompt = prompt[:-len(self.sys_ps1)]
1389        self.text.tag_add("console", "iomark-1c")
1390        self.console.write(prompt)
1391
1392        self.shell_sidebar.update_sidebar()
1393        self.text.mark_set("insert", "end-1c")
1394        self.set_line_and_column()
1395        self.io.reset_undo()
1396
1397    def show_warning(self, msg):
1398        width = self.interp.tkconsole.width
1399        wrapper = TextWrapper(width=width, tabsize=8, expand_tabs=True)
1400        wrapped_msg = '\n'.join(wrapper.wrap(msg))
1401        if not wrapped_msg.endswith('\n'):
1402            wrapped_msg += '\n'
1403        self.per.bottom.insert("iomark linestart", wrapped_msg, "stderr")
1404
1405    def resetoutput(self):
1406        source = self.text.get("iomark", "end-1c")
1407        if self.history:
1408            self.history.store(source)
1409        if self.text.get("end-2c") != "\n":
1410            self.text.insert("end-1c", "\n")
1411        self.text.mark_set("iomark", "end-1c")
1412        self.set_line_and_column()
1413        self.ctip.remove_calltip_window()
1414
1415    def write(self, s, tags=()):
1416        try:
1417            self.text.mark_gravity("iomark", "right")
1418            count = OutputWindow.write(self, s, tags, "iomark")
1419            self.text.mark_gravity("iomark", "left")
1420        except:
1421            raise ###pass  # ### 11Aug07 KBK if we are expecting exceptions
1422                           # let's find out what they are and be specific.
1423        if self.canceled:
1424            self.canceled = False
1425            if not use_subprocess:
1426                raise KeyboardInterrupt
1427        return count
1428
1429    def rmenu_check_cut(self):
1430        try:
1431            if self.text.compare('sel.first', '<', 'iomark'):
1432                return 'disabled'
1433        except TclError: # no selection, so the index 'sel.first' doesn't exist
1434            return 'disabled'
1435        return super().rmenu_check_cut()
1436
1437    def rmenu_check_paste(self):
1438        if self.text.compare('insert','<','iomark'):
1439            return 'disabled'
1440        return super().rmenu_check_paste()
1441
1442    def squeeze_current_text_event(self, event=None):
1443        self.squeezer.squeeze_current_text()
1444        self.shell_sidebar.update_sidebar()
1445
1446    def on_squeezed_expand(self, index, text, tags):
1447        self.shell_sidebar.update_sidebar()
1448
1449
1450def fix_x11_paste(root):
1451    "Make paste replace selection on x11.  See issue #5124."
1452    if root._windowingsystem == 'x11':
1453        for cls in 'Text', 'Entry', 'Spinbox':
1454            root.bind_class(
1455                cls,
1456                '<<Paste>>',
1457                'catch {%W delete sel.first sel.last}\n' +
1458                        root.bind_class(cls, '<<Paste>>'))
1459
1460
1461usage_msg = """\
1462
1463USAGE: idle  [-deins] [-t title] [file]*
1464       idle  [-dns] [-t title] (-c cmd | -r file) [arg]*
1465       idle  [-dns] [-t title] - [arg]*
1466
1467  -h         print this help message and exit
1468  -n         run IDLE without a subprocess (DEPRECATED,
1469             see Help/IDLE Help for details)
1470
1471The following options will override the IDLE 'settings' configuration:
1472
1473  -e         open an edit window
1474  -i         open a shell window
1475
1476The following options imply -i and will open a shell:
1477
1478  -c cmd     run the command in a shell, or
1479  -r file    run script from file
1480
1481  -d         enable the debugger
1482  -s         run $IDLESTARTUP or $PYTHONSTARTUP before anything else
1483  -t title   set title of shell window
1484
1485A default edit window will be bypassed when -c, -r, or - are used.
1486
1487[arg]* are passed to the command (-c) or script (-r) in sys.argv[1:].
1488
1489Examples:
1490
1491idle
1492        Open an edit window or shell depending on IDLE's configuration.
1493
1494idle foo.py foobar.py
1495        Edit the files, also open a shell if configured to start with shell.
1496
1497idle -est "Baz" foo.py
1498        Run $IDLESTARTUP or $PYTHONSTARTUP, edit foo.py, and open a shell
1499        window with the title "Baz".
1500
1501idle -c "import sys; print(sys.argv)" "foo"
1502        Open a shell window and run the command, passing "-c" in sys.argv[0]
1503        and "foo" in sys.argv[1].
1504
1505idle -d -s -r foo.py "Hello World"
1506        Open a shell window, run a startup script, enable the debugger, and
1507        run foo.py, passing "foo.py" in sys.argv[0] and "Hello World" in
1508        sys.argv[1].
1509
1510echo "import sys; print(sys.argv)" | idle - "foobar"
1511        Open a shell window, run the script piped in, passing '' in sys.argv[0]
1512        and "foobar" in sys.argv[1].
1513"""
1514
1515def main():
1516    import getopt
1517    from platform import system
1518    from idlelib import testing  # bool value
1519    from idlelib import macosx
1520
1521    global flist, root, use_subprocess
1522
1523    capture_warnings(True)
1524    use_subprocess = True
1525    enable_shell = False
1526    enable_edit = False
1527    debug = False
1528    cmd = None
1529    script = None
1530    startup = False
1531    try:
1532        opts, args = getopt.getopt(sys.argv[1:], "c:deihnr:st:")
1533    except getopt.error as msg:
1534        print(f"Error: {msg}\n{usage_msg}", file=sys.stderr)
1535        sys.exit(2)
1536    for o, a in opts:
1537        if o == '-c':
1538            cmd = a
1539            enable_shell = True
1540        if o == '-d':
1541            debug = True
1542            enable_shell = True
1543        if o == '-e':
1544            enable_edit = True
1545        if o == '-h':
1546            sys.stdout.write(usage_msg)
1547            sys.exit()
1548        if o == '-i':
1549            enable_shell = True
1550        if o == '-n':
1551            print(" Warning: running IDLE without a subprocess is deprecated.",
1552                  file=sys.stderr)
1553            use_subprocess = False
1554        if o == '-r':
1555            script = a
1556            if os.path.isfile(script):
1557                pass
1558            else:
1559                print("No script file: ", script)
1560                sys.exit()
1561            enable_shell = True
1562        if o == '-s':
1563            startup = True
1564            enable_shell = True
1565        if o == '-t':
1566            PyShell.shell_title = a
1567            enable_shell = True
1568    if args and args[0] == '-':
1569        cmd = sys.stdin.read()
1570        enable_shell = True
1571    # process sys.argv and sys.path:
1572    for i in range(len(sys.path)):
1573        sys.path[i] = os.path.abspath(sys.path[i])
1574    if args and args[0] == '-':
1575        sys.argv = [''] + args[1:]
1576    elif cmd:
1577        sys.argv = ['-c'] + args
1578    elif script:
1579        sys.argv = [script] + args
1580    elif args:
1581        enable_edit = True
1582        pathx = []
1583        for filename in args:
1584            pathx.append(os.path.dirname(filename))
1585        for dir in pathx:
1586            dir = os.path.abspath(dir)
1587            if not dir in sys.path:
1588                sys.path.insert(0, dir)
1589    else:
1590        dir = os.getcwd()
1591        if dir not in sys.path:
1592            sys.path.insert(0, dir)
1593    # check the IDLE settings configuration (but command line overrides)
1594    edit_start = idleConf.GetOption('main', 'General',
1595                                    'editor-on-startup', type='bool')
1596    enable_edit = enable_edit or edit_start
1597    enable_shell = enable_shell or not enable_edit
1598
1599    # Setup root.  Don't break user code run in IDLE process.
1600    # Don't change environment when testing.
1601    if use_subprocess and not testing:
1602        NoDefaultRoot()
1603    root = Tk(className="Idle")
1604    root.withdraw()
1605    from idlelib.run import fix_scaling
1606    fix_scaling(root)
1607
1608    # set application icon
1609    icondir = os.path.join(os.path.dirname(__file__), 'Icons')
1610    if system() == 'Windows':
1611        iconfile = os.path.join(icondir, 'idle.ico')
1612        root.wm_iconbitmap(default=iconfile)
1613    elif not macosx.isAquaTk():
1614        if TkVersion >= 8.6:
1615            ext = '.png'
1616            sizes = (16, 32, 48, 256)
1617        else:
1618            ext = '.gif'
1619            sizes = (16, 32, 48)
1620        iconfiles = [os.path.join(icondir, 'idle_%d%s' % (size, ext))
1621                     for size in sizes]
1622        icons = [PhotoImage(master=root, file=iconfile)
1623                 for iconfile in iconfiles]
1624        root.wm_iconphoto(True, *icons)
1625
1626    # start editor and/or shell windows:
1627    fixwordbreaks(root)
1628    fix_x11_paste(root)
1629    flist = PyShellFileList(root)
1630    macosx.setupApp(root, flist)
1631
1632    if enable_edit:
1633        if not (cmd or script):
1634            for filename in args[:]:
1635                if flist.open(filename) is None:
1636                    # filename is a directory actually, disconsider it
1637                    args.remove(filename)
1638            if not args:
1639                flist.new()
1640
1641    if enable_shell:
1642        shell = flist.open_shell()
1643        if not shell:
1644            return # couldn't open shell
1645        if macosx.isAquaTk() and flist.dict:
1646            # On OSX: when the user has double-clicked on a file that causes
1647            # IDLE to be launched the shell window will open just in front of
1648            # the file she wants to see. Lower the interpreter window when
1649            # there are open files.
1650            shell.top.lower()
1651    else:
1652        shell = flist.pyshell
1653
1654    # Handle remaining options. If any of these are set, enable_shell
1655    # was set also, so shell must be true to reach here.
1656    if debug:
1657        shell.open_debugger()
1658    if startup:
1659        filename = os.environ.get("IDLESTARTUP") or \
1660                   os.environ.get("PYTHONSTARTUP")
1661        if filename and os.path.isfile(filename):
1662            shell.interp.execfile(filename)
1663    if cmd or script:
1664        shell.interp.runcommand("""if 1:
1665            import sys as _sys
1666            _sys.argv = {!r}
1667            del _sys
1668            \n""".format(sys.argv))
1669        if cmd:
1670            shell.interp.execsource(cmd)
1671        elif script:
1672            shell.interp.prepend_syspath(script)
1673            shell.interp.execfile(script)
1674    elif shell:
1675        # If there is a shell window and no cmd or script in progress,
1676        # check for problematic issues and print warning message(s) in
1677        # the IDLE shell window; this is less intrusive than always
1678        # opening a separate window.
1679
1680        # Warn if the "Prefer tabs when opening documents" system
1681        # preference is set to "Always".
1682        prefer_tabs_preference_warning = macosx.preferTabsPreferenceWarning()
1683        if prefer_tabs_preference_warning:
1684            shell.show_warning(prefer_tabs_preference_warning)
1685
1686    while flist.inversedict:  # keep IDLE running while files are open.
1687        root.mainloop()
1688    root.destroy()
1689    capture_warnings(False)
1690
1691
1692if __name__ == "__main__":
1693    main()
1694
1695capture_warnings(False)  # Make sure turned off; see issue 18081
1696