1 """ idlelib.run
2
3 Simplified, pyshell.ModifiedInterpreter spawns a subprocess with
4 f'''{sys.executable} -c "__import__('idlelib.run').run.main()"'''
5 '.run' is needed because __import__ returns idlelib, not idlelib.run.
6 """
7 import contextlib
8 import functools
9 import io
10 import linecache
11 import queue
12 import sys
13 import textwrap
14 import time
15 import traceback
16 import _thread as thread
17 import threading
18 import warnings
19
20 import idlelib # testing
21 from idlelib import autocomplete # AutoComplete, fetch_encodings
22 from idlelib import calltip # Calltip
23 from idlelib import debugger_r # start_debugger
24 from idlelib import debugobj_r # remote_object_tree_item
25 from idlelib import iomenu # encoding
26 from idlelib import rpc # multiple objects
27 from idlelib import stackviewer # StackTreeItem
28 import __main__
29
30 import tkinter # Use tcl and, if startup fails, messagebox.
31 if not hasattr(sys.modules['idlelib.run'], 'firstrun'):
32 # Undo modifications of tkinter by idlelib imports; see bpo-25507.
33 for mod in ('simpledialog', 'messagebox', 'font',
34 'dialog', 'filedialog', 'commondialog',
35 'ttk'):
36 delattr(tkinter, mod)
37 del sys.modules['tkinter.' + mod]
38 # Avoid AttributeError if run again; see bpo-37038.
39 sys.modules['idlelib.run'].firstrun = False
40
41 LOCALHOST = '127.0.0.1'
42
43 try:
44 eof = 'Ctrl-D (end-of-file)'
45 exit.eof = eof
46 quit.eof = eof
47 except NameError: # In case subprocess started with -S (maybe in future).
48 pass
49
50
51 def idle_formatwarning(message, category, filename, lineno, line=None):
52 """Format warnings the IDLE way."""
53
54 s = "\nWarning (from warnings module):\n"
55 s += f' File \"{filename}\", line {lineno}\n'
56 if line is None:
57 line = linecache.getline(filename, lineno)
58 line = line.strip()
59 if line:
60 s += " %s\n" % line
61 s += f"{category.__name__}: {message}\n"
62 return s
63
64 def idle_showwarning_subproc(
65 message, category, filename, lineno, file=None, line=None):
66 """Show Idle-format warning after replacing warnings.showwarning.
67
68 The only difference is the formatter called.
69 """
70 if file is None:
71 file = sys.stderr
72 try:
73 file.write(idle_formatwarning(
74 message, category, filename, lineno, line))
75 except OSError:
76 pass # the file (probably stderr) is invalid - this warning gets lost.
77
78 _warnings_showwarning = None
79
80 def capture_warnings(capture):
81 "Replace warning.showwarning with idle_showwarning_subproc, or reverse."
82
83 global _warnings_showwarning
84 if capture:
85 if _warnings_showwarning is None:
86 _warnings_showwarning = warnings.showwarning
87 warnings.showwarning = idle_showwarning_subproc
88 else:
89 if _warnings_showwarning is not None:
90 warnings.showwarning = _warnings_showwarning
91 _warnings_showwarning = None
92
93 capture_warnings(True)
94 tcl = tkinter.Tcl()
95
96 def handle_tk_events(tcl=tcl):
97 """Process any tk events that are ready to be dispatched if tkinter
98 has been imported, a tcl interpreter has been created and tk has been
99 loaded."""
100 tcl.eval("update")
101
102 # Thread shared globals: Establish a queue between a subthread (which handles
103 # the socket) and the main thread (which runs user code), plus global
104 # completion, exit and interruptable (the main thread) flags:
105
106 exit_now = False
107 quitting = False
108 interruptable = False
109
110 def main(del_exitfunc=False):
111 """Start the Python execution server in a subprocess
112
113 In the Python subprocess, RPCServer is instantiated with handlerclass
114 MyHandler, which inherits register/unregister methods from RPCHandler via
115 the mix-in class SocketIO.
116
117 When the RPCServer 'server' is instantiated, the TCPServer initialization
118 creates an instance of run.MyHandler and calls its handle() method.
119 handle() instantiates a run.Executive object, passing it a reference to the
120 MyHandler object. That reference is saved as attribute rpchandler of the
121 Executive instance. The Executive methods have access to the reference and
122 can pass it on to entities that they command
123 (e.g. debugger_r.Debugger.start_debugger()). The latter, in turn, can
124 call MyHandler(SocketIO) register/unregister methods via the reference to
125 register and unregister themselves.
126
127 """
128 global exit_now
129 global quitting
130 global no_exitfunc
131 no_exitfunc = del_exitfunc
132 #time.sleep(15) # test subprocess not responding
133 try:
134 assert(len(sys.argv) > 1)
135 port = int(sys.argv[-1])
136 except:
137 print("IDLE Subprocess: no IP port passed in sys.argv.",
138 file=sys.__stderr__)
139 return
140
141 capture_warnings(True)
142 sys.argv[:] = [""]
143 threading.Thread(target=manage_socket,
144 name='SockThread',
145 args=((LOCALHOST, port),),
146 daemon=True,
147 ).start()
148
149 while True:
150 try:
151 if exit_now:
152 try:
153 exit()
154 except KeyboardInterrupt:
155 # exiting but got an extra KBI? Try again!
156 continue
157 try:
158 request = rpc.request_queue.get(block=True, timeout=0.05)
159 except queue.Empty:
160 request = None
161 # Issue 32207: calling handle_tk_events here adds spurious
162 # queue.Empty traceback to event handling exceptions.
163 if request:
164 seq, (method, args, kwargs) = request
165 ret = method(*args, **kwargs)
166 rpc.response_queue.put((seq, ret))
167 else:
168 handle_tk_events()
169 except KeyboardInterrupt:
170 if quitting:
171 exit_now = True
172 continue
173 except SystemExit:
174 capture_warnings(False)
175 raise
176 except:
177 type, value, tb = sys.exc_info()
178 try:
179 print_exception()
180 rpc.response_queue.put((seq, None))
181 except:
182 # Link didn't work, print same exception to __stderr__
183 traceback.print_exception(type, value, tb, file=sys.__stderr__)
184 exit()
185 else:
186 continue
187
188 def manage_socket(address):
189 for i in range(3):
190 time.sleep(i)
191 try:
192 server = MyRPCServer(address, MyHandler)
193 break
194 except OSError as err:
195 print("IDLE Subprocess: OSError: " + err.args[1] +
196 ", retrying....", file=sys.__stderr__)
197 socket_error = err
198 else:
199 print("IDLE Subprocess: Connection to "
200 "IDLE GUI failed, exiting.", file=sys.__stderr__)
201 show_socket_error(socket_error, address)
202 global exit_now
203 exit_now = True
204 return
205 server.handle_request() # A single request only
206
207 def show_socket_error(err, address):
208 "Display socket error from manage_socket."
209 import tkinter
210 from tkinter.messagebox import showerror
211 root = tkinter.Tk()
212 fix_scaling(root)
213 root.withdraw()
214 showerror(
215 "Subprocess Connection Error",
216 f"IDLE's subprocess can't connect to {address[0]}:{address[1]}.\n"
217 f"Fatal OSError #{err.errno}: {err.strerror}.\n"
218 "See the 'Startup failure' section of the IDLE doc, online at\n"
219 "https://docs.python.org/3/library/idle.html#startup-failure",
220 parent=root)
221 root.destroy()
222
223
224 def get_message_lines(typ, exc, tb):
225 "Return line composing the exception message."
226 if typ in (AttributeError, NameError):
227 # 3.10+ hints are not directly accessible from python (#44026).
228 err = io.StringIO()
229 with contextlib.redirect_stderr(err):
230 sys.__excepthook__(typ, exc, tb)
231 return [err.getvalue().split("\n")[-2] + "\n"]
232 else:
233 return traceback.format_exception_only(typ, exc)
234
235
236 def print_exception():
237 import linecache
238 linecache.checkcache()
239 flush_stdout()
240 efile = sys.stderr
241 typ, val, tb = excinfo = sys.exc_info()
242 sys.last_type, sys.last_value, sys.last_traceback = excinfo
243 seen = set()
244
245 def print_exc(typ, exc, tb):
246 seen.add(id(exc))
247 context = exc.__context__
248 cause = exc.__cause__
249 if cause is not None and id(cause) not in seen:
250 print_exc(type(cause), cause, cause.__traceback__)
251 print("\nThe above exception was the direct cause "
252 "of the following exception:\n", file=efile)
253 elif (context is not None and
254 not exc.__suppress_context__ and
255 id(context) not in seen):
256 print_exc(type(context), context, context.__traceback__)
257 print("\nDuring handling of the above exception, "
258 "another exception occurred:\n", file=efile)
259 if tb:
260 tbe = traceback.extract_tb(tb)
261 print('Traceback (most recent call last):', file=efile)
262 exclude = ("run.py", "rpc.py", "threading.py", "queue.py",
263 "debugger_r.py", "bdb.py")
264 cleanup_traceback(tbe, exclude)
265 traceback.print_list(tbe, file=efile)
266 lines = get_message_lines(typ, exc, tb)
267 for line in lines:
268 print(line, end='', file=efile)
269
270 print_exc(typ, val, tb)
271
272 def cleanup_traceback(tb, exclude):
273 "Remove excluded traces from beginning/end of tb; get cached lines"
274 orig_tb = tb[:]
275 while tb:
276 for rpcfile in exclude:
277 if tb[0][0].count(rpcfile):
278 break # found an exclude, break for: and delete tb[0]
279 else:
280 break # no excludes, have left RPC code, break while:
281 del tb[0]
282 while tb:
283 for rpcfile in exclude:
284 if tb[-1][0].count(rpcfile):
285 break
286 else:
287 break
288 del tb[-1]
289 if len(tb) == 0:
290 # exception was in IDLE internals, don't prune!
291 tb[:] = orig_tb[:]
292 print("** IDLE Internal Exception: ", file=sys.stderr)
293 rpchandler = rpc.objecttable['exec'].rpchandler
294 for i in range(len(tb)):
295 fn, ln, nm, line = tb[i]
296 if nm == '?':
297 nm = "-toplevel-"
298 if not line and fn.startswith("<pyshell#"):
299 line = rpchandler.remotecall('linecache', 'getline',
300 (fn, ln), {})
301 tb[i] = fn, ln, nm, line
302
303 def flush_stdout():
304 """XXX How to do this now?"""
305
306 def exit():
307 """Exit subprocess, possibly after first clearing exit functions.
308
309 If config-main.cfg/.def 'General' 'delete-exitfunc' is True, then any
310 functions registered with atexit will be removed before exiting.
311 (VPython support)
312
313 """
314 if no_exitfunc:
315 import atexit
316 atexit._clear()
317 capture_warnings(False)
318 sys.exit(0)
319
320
321 def fix_scaling(root):
322 """Scale fonts on HiDPI displays."""
323 import tkinter.font
324 scaling = float(root.tk.call('tk', 'scaling'))
325 if scaling > 1.4:
326 for name in tkinter.font.names(root):
327 font = tkinter.font.Font(root=root, name=name, exists=True)
328 size = int(font['size'])
329 if size < 0:
330 font['size'] = round(-0.75*size)
331
332
333 def fixdoc(fun, text):
334 tem = (fun.__doc__ + '\n\n') if fun.__doc__ is not None else ''
335 fun.__doc__ = tem + textwrap.fill(textwrap.dedent(text))
336
337 RECURSIONLIMIT_DELTA = 30
338
339 def install_recursionlimit_wrappers():
340 """Install wrappers to always add 30 to the recursion limit."""
341 # see: bpo-26806
342
343 @functools.wraps(sys.setrecursionlimit)
344 def setrecursionlimit(*args, **kwargs):
345 # mimic the original sys.setrecursionlimit()'s input handling
346 if kwargs:
347 raise TypeError(
348 "setrecursionlimit() takes no keyword arguments")
349 try:
350 limit, = args
351 except ValueError:
352 raise TypeError(f"setrecursionlimit() takes exactly one "
353 f"argument ({len(args)} given)")
354 if not limit > 0:
355 raise ValueError(
356 "recursion limit must be greater or equal than 1")
357
358 return setrecursionlimit.__wrapped__(limit + RECURSIONLIMIT_DELTA)
359
360 fixdoc(setrecursionlimit, f"""\
361 This IDLE wrapper adds {RECURSIONLIMIT_DELTA} to prevent possible
362 uninterruptible loops.""")
363
364 @functools.wraps(sys.getrecursionlimit)
365 def getrecursionlimit():
366 return getrecursionlimit.__wrapped__() - RECURSIONLIMIT_DELTA
367
368 fixdoc(getrecursionlimit, f"""\
369 This IDLE wrapper subtracts {RECURSIONLIMIT_DELTA} to compensate
370 for the {RECURSIONLIMIT_DELTA} IDLE adds when setting the limit.""")
371
372 # add the delta to the default recursion limit, to compensate
373 sys.setrecursionlimit(sys.getrecursionlimit() + RECURSIONLIMIT_DELTA)
374
375 sys.setrecursionlimit = setrecursionlimit
376 sys.getrecursionlimit = getrecursionlimit
377
378
379 def uninstall_recursionlimit_wrappers():
380 """Uninstall the recursion limit wrappers from the sys module.
381
382 IDLE only uses this for tests. Users can import run and call
383 this to remove the wrapping.
384 """
385 if (
386 getattr(sys.setrecursionlimit, '__wrapped__', None) and
387 getattr(sys.getrecursionlimit, '__wrapped__', None)
388 ):
389 sys.setrecursionlimit = sys.setrecursionlimit.__wrapped__
390 sys.getrecursionlimit = sys.getrecursionlimit.__wrapped__
391 sys.setrecursionlimit(sys.getrecursionlimit() - RECURSIONLIMIT_DELTA)
392
393
394 class ESC[4;38;5;81mMyRPCServer(ESC[4;38;5;149mrpcESC[4;38;5;149m.ESC[4;38;5;149mRPCServer):
395
396 def handle_error(self, request, client_address):
397 """Override RPCServer method for IDLE
398
399 Interrupt the MainThread and exit server if link is dropped.
400
401 """
402 global quitting
403 try:
404 raise
405 except SystemExit:
406 raise
407 except EOFError:
408 global exit_now
409 exit_now = True
410 thread.interrupt_main()
411 except:
412 erf = sys.__stderr__
413 print(textwrap.dedent(f"""
414 {'-'*40}
415 Unhandled exception in user code execution server!'
416 Thread: {threading.current_thread().name}
417 IDLE Client Address: {client_address}
418 Request: {request!r}
419 """), file=erf)
420 traceback.print_exc(limit=-20, file=erf)
421 print(textwrap.dedent(f"""
422 *** Unrecoverable, server exiting!
423
424 Users should never see this message; it is likely transient.
425 If this recurs, report this with a copy of the message
426 and an explanation of how to make it repeat.
427 {'-'*40}"""), file=erf)
428 quitting = True
429 thread.interrupt_main()
430
431
432 # Pseudofiles for shell-remote communication (also used in pyshell)
433
434 class ESC[4;38;5;81mStdioFile(ESC[4;38;5;149mioESC[4;38;5;149m.ESC[4;38;5;149mTextIOBase):
435
436 def __init__(self, shell, tags, encoding='utf-8', errors='strict'):
437 self.shell = shell
438 self.tags = tags
439 self._encoding = encoding
440 self._errors = errors
441
442 @property
443 def encoding(self):
444 return self._encoding
445
446 @property
447 def errors(self):
448 return self._errors
449
450 @property
451 def name(self):
452 return '<%s>' % self.tags
453
454 def isatty(self):
455 return True
456
457
458 class ESC[4;38;5;81mStdOutputFile(ESC[4;38;5;149mStdioFile):
459
460 def writable(self):
461 return True
462
463 def write(self, s):
464 if self.closed:
465 raise ValueError("write to closed file")
466 s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors)
467 return self.shell.write(s, self.tags)
468
469
470 class ESC[4;38;5;81mStdInputFile(ESC[4;38;5;149mStdioFile):
471 _line_buffer = ''
472
473 def readable(self):
474 return True
475
476 def read(self, size=-1):
477 if self.closed:
478 raise ValueError("read from closed file")
479 if size is None:
480 size = -1
481 elif not isinstance(size, int):
482 raise TypeError('must be int, not ' + type(size).__name__)
483 result = self._line_buffer
484 self._line_buffer = ''
485 if size < 0:
486 while line := self.shell.readline():
487 result += line
488 else:
489 while len(result) < size:
490 line = self.shell.readline()
491 if not line: break
492 result += line
493 self._line_buffer = result[size:]
494 result = result[:size]
495 return result
496
497 def readline(self, size=-1):
498 if self.closed:
499 raise ValueError("read from closed file")
500 if size is None:
501 size = -1
502 elif not isinstance(size, int):
503 raise TypeError('must be int, not ' + type(size).__name__)
504 line = self._line_buffer or self.shell.readline()
505 if size < 0:
506 size = len(line)
507 eol = line.find('\n', 0, size)
508 if eol >= 0:
509 size = eol + 1
510 self._line_buffer = line[size:]
511 return line[:size]
512
513 def close(self):
514 self.shell.close()
515
516
517 class ESC[4;38;5;81mMyHandler(ESC[4;38;5;149mrpcESC[4;38;5;149m.ESC[4;38;5;149mRPCHandler):
518
519 def handle(self):
520 """Override base method"""
521 executive = Executive(self)
522 self.register("exec", executive)
523 self.console = self.get_remote_proxy("console")
524 sys.stdin = StdInputFile(self.console, "stdin",
525 iomenu.encoding, iomenu.errors)
526 sys.stdout = StdOutputFile(self.console, "stdout",
527 iomenu.encoding, iomenu.errors)
528 sys.stderr = StdOutputFile(self.console, "stderr",
529 iomenu.encoding, "backslashreplace")
530
531 sys.displayhook = rpc.displayhook
532 # page help() text to shell.
533 import pydoc # import must be done here to capture i/o binding
534 pydoc.pager = pydoc.plainpager
535
536 # Keep a reference to stdin so that it won't try to exit IDLE if
537 # sys.stdin gets changed from within IDLE's shell. See issue17838.
538 self._keep_stdin = sys.stdin
539
540 install_recursionlimit_wrappers()
541
542 self.interp = self.get_remote_proxy("interp")
543 rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
544
545 def exithook(self):
546 "override SocketIO method - wait for MainThread to shut us down"
547 time.sleep(10)
548
549 def EOFhook(self):
550 "Override SocketIO method - terminate wait on callback and exit thread"
551 global quitting
552 quitting = True
553 thread.interrupt_main()
554
555 def decode_interrupthook(self):
556 "interrupt awakened thread"
557 global quitting
558 quitting = True
559 thread.interrupt_main()
560
561
562 class ESC[4;38;5;81mExecutive:
563
564 def __init__(self, rpchandler):
565 self.rpchandler = rpchandler
566 if idlelib.testing is False:
567 self.locals = __main__.__dict__
568 self.calltip = calltip.Calltip()
569 self.autocomplete = autocomplete.AutoComplete()
570 else:
571 self.locals = {}
572
573 def runcode(self, code):
574 global interruptable
575 try:
576 self.user_exc_info = None
577 interruptable = True
578 try:
579 exec(code, self.locals)
580 finally:
581 interruptable = False
582 except SystemExit as e:
583 if e.args: # SystemExit called with an argument.
584 ob = e.args[0]
585 if not isinstance(ob, (type(None), int)):
586 print('SystemExit: ' + str(ob), file=sys.stderr)
587 # Return to the interactive prompt.
588 except:
589 self.user_exc_info = sys.exc_info() # For testing, hook, viewer.
590 if quitting:
591 exit()
592 if sys.excepthook is sys.__excepthook__:
593 print_exception()
594 else:
595 try:
596 sys.excepthook(*self.user_exc_info)
597 except:
598 self.user_exc_info = sys.exc_info() # For testing.
599 print_exception()
600 jit = self.rpchandler.console.getvar("<<toggle-jit-stack-viewer>>")
601 if jit:
602 self.rpchandler.interp.open_remote_stack_viewer()
603 else:
604 flush_stdout()
605
606 def interrupt_the_server(self):
607 if interruptable:
608 thread.interrupt_main()
609
610 def start_the_debugger(self, gui_adap_oid):
611 return debugger_r.start_debugger(self.rpchandler, gui_adap_oid)
612
613 def stop_the_debugger(self, idb_adap_oid):
614 "Unregister the Idb Adapter. Link objects and Idb then subject to GC"
615 self.rpchandler.unregister(idb_adap_oid)
616
617 def get_the_calltip(self, name):
618 return self.calltip.fetch_tip(name)
619
620 def get_the_completion_list(self, what, mode):
621 return self.autocomplete.fetch_completions(what, mode)
622
623 def stackviewer(self, flist_oid=None):
624 if self.user_exc_info:
625 _, exc, tb = self.user_exc_info
626 else:
627 return None
628 flist = None
629 if flist_oid is not None:
630 flist = self.rpchandler.get_remote_proxy(flist_oid)
631 while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]:
632 tb = tb.tb_next
633 exc.__traceback__ = tb
634 item = stackviewer.StackTreeItem(exc, flist)
635 return debugobj_r.remote_object_tree_item(item)
636
637
638 if __name__ == '__main__':
639 from unittest import main
640 main('idlelib.idle_test.test_run', verbosity=2)
641
642 capture_warnings(False) # Make sure turned off; see bpo-18081.