python (3.12.0)
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 sys.last_exc = val
244 seen = set()
245
246 def print_exc(typ, exc, tb):
247 seen.add(id(exc))
248 context = exc.__context__
249 cause = exc.__cause__
250 if cause is not None and id(cause) not in seen:
251 print_exc(type(cause), cause, cause.__traceback__)
252 print("\nThe above exception was the direct cause "
253 "of the following exception:\n", file=efile)
254 elif (context is not None and
255 not exc.__suppress_context__ and
256 id(context) not in seen):
257 print_exc(type(context), context, context.__traceback__)
258 print("\nDuring handling of the above exception, "
259 "another exception occurred:\n", file=efile)
260 if tb:
261 tbe = traceback.extract_tb(tb)
262 print('Traceback (most recent call last):', file=efile)
263 exclude = ("run.py", "rpc.py", "threading.py", "queue.py",
264 "debugger_r.py", "bdb.py")
265 cleanup_traceback(tbe, exclude)
266 traceback.print_list(tbe, file=efile)
267 lines = get_message_lines(typ, exc, tb)
268 for line in lines:
269 print(line, end='', file=efile)
270
271 print_exc(typ, val, tb)
272
273 def cleanup_traceback(tb, exclude):
274 "Remove excluded traces from beginning/end of tb; get cached lines"
275 orig_tb = tb[:]
276 while tb:
277 for rpcfile in exclude:
278 if tb[0][0].count(rpcfile):
279 break # found an exclude, break for: and delete tb[0]
280 else:
281 break # no excludes, have left RPC code, break while:
282 del tb[0]
283 while tb:
284 for rpcfile in exclude:
285 if tb[-1][0].count(rpcfile):
286 break
287 else:
288 break
289 del tb[-1]
290 if len(tb) == 0:
291 # exception was in IDLE internals, don't prune!
292 tb[:] = orig_tb[:]
293 print("** IDLE Internal Exception: ", file=sys.stderr)
294 rpchandler = rpc.objecttable['exec'].rpchandler
295 for i in range(len(tb)):
296 fn, ln, nm, line = tb[i]
297 if nm == '?':
298 nm = "-toplevel-"
299 if not line and fn.startswith("<pyshell#"):
300 line = rpchandler.remotecall('linecache', 'getline',
301 (fn, ln), {})
302 tb[i] = fn, ln, nm, line
303
304 def flush_stdout():
305 """XXX How to do this now?"""
306
307 def exit():
308 """Exit subprocess, possibly after first clearing exit functions.
309
310 If config-main.cfg/.def 'General' 'delete-exitfunc' is True, then any
311 functions registered with atexit will be removed before exiting.
312 (VPython support)
313
314 """
315 if no_exitfunc:
316 import atexit
317 atexit._clear()
318 capture_warnings(False)
319 sys.exit(0)
320
321
322 def fix_scaling(root):
323 """Scale fonts on HiDPI displays."""
324 import tkinter.font
325 scaling = float(root.tk.call('tk', 'scaling'))
326 if scaling > 1.4:
327 for name in tkinter.font.names(root):
328 font = tkinter.font.Font(root=root, name=name, exists=True)
329 size = int(font['size'])
330 if size < 0:
331 font['size'] = round(-0.75*size)
332
333
334 def fixdoc(fun, text):
335 tem = (fun.__doc__ + '\n\n') if fun.__doc__ is not None else ''
336 fun.__doc__ = tem + textwrap.fill(textwrap.dedent(text))
337
338 RECURSIONLIMIT_DELTA = 30
339
340 def install_recursionlimit_wrappers():
341 """Install wrappers to always add 30 to the recursion limit."""
342 # see: bpo-26806
343
344 @functools.wraps(sys.setrecursionlimit)
345 def setrecursionlimit(*args, **kwargs):
346 # mimic the original sys.setrecursionlimit()'s input handling
347 if kwargs:
348 raise TypeError(
349 "setrecursionlimit() takes no keyword arguments")
350 try:
351 limit, = args
352 except ValueError:
353 raise TypeError(f"setrecursionlimit() takes exactly one "
354 f"argument ({len(args)} given)")
355 if not limit > 0:
356 raise ValueError(
357 "recursion limit must be greater or equal than 1")
358
359 return setrecursionlimit.__wrapped__(limit + RECURSIONLIMIT_DELTA)
360
361 fixdoc(setrecursionlimit, f"""\
362 This IDLE wrapper adds {RECURSIONLIMIT_DELTA} to prevent possible
363 uninterruptible loops.""")
364
365 @functools.wraps(sys.getrecursionlimit)
366 def getrecursionlimit():
367 return getrecursionlimit.__wrapped__() - RECURSIONLIMIT_DELTA
368
369 fixdoc(getrecursionlimit, f"""\
370 This IDLE wrapper subtracts {RECURSIONLIMIT_DELTA} to compensate
371 for the {RECURSIONLIMIT_DELTA} IDLE adds when setting the limit.""")
372
373 # add the delta to the default recursion limit, to compensate
374 sys.setrecursionlimit(sys.getrecursionlimit() + RECURSIONLIMIT_DELTA)
375
376 sys.setrecursionlimit = setrecursionlimit
377 sys.getrecursionlimit = getrecursionlimit
378
379
380 def uninstall_recursionlimit_wrappers():
381 """Uninstall the recursion limit wrappers from the sys module.
382
383 IDLE only uses this for tests. Users can import run and call
384 this to remove the wrapping.
385 """
386 if (
387 getattr(sys.setrecursionlimit, '__wrapped__', None) and
388 getattr(sys.getrecursionlimit, '__wrapped__', None)
389 ):
390 sys.setrecursionlimit = sys.setrecursionlimit.__wrapped__
391 sys.getrecursionlimit = sys.getrecursionlimit.__wrapped__
392 sys.setrecursionlimit(sys.getrecursionlimit() - RECURSIONLIMIT_DELTA)
393
394
395 class ESC[4;38;5;81mMyRPCServer(ESC[4;38;5;149mrpcESC[4;38;5;149m.ESC[4;38;5;149mRPCServer):
396
397 def handle_error(self, request, client_address):
398 """Override RPCServer method for IDLE
399
400 Interrupt the MainThread and exit server if link is dropped.
401
402 """
403 global quitting
404 try:
405 raise
406 except SystemExit:
407 raise
408 except EOFError:
409 global exit_now
410 exit_now = True
411 thread.interrupt_main()
412 except:
413 erf = sys.__stderr__
414 print(textwrap.dedent(f"""
415 {'-'*40}
416 Unhandled exception in user code execution server!'
417 Thread: {threading.current_thread().name}
418 IDLE Client Address: {client_address}
419 Request: {request!r}
420 """), file=erf)
421 traceback.print_exc(limit=-20, file=erf)
422 print(textwrap.dedent(f"""
423 *** Unrecoverable, server exiting!
424
425 Users should never see this message; it is likely transient.
426 If this recurs, report this with a copy of the message
427 and an explanation of how to make it repeat.
428 {'-'*40}"""), file=erf)
429 quitting = True
430 thread.interrupt_main()
431
432
433 # Pseudofiles for shell-remote communication (also used in pyshell)
434
435 class ESC[4;38;5;81mStdioFile(ESC[4;38;5;149mioESC[4;38;5;149m.ESC[4;38;5;149mTextIOBase):
436
437 def __init__(self, shell, tags, encoding='utf-8', errors='strict'):
438 self.shell = shell
439 self.tags = tags
440 self._encoding = encoding
441 self._errors = errors
442
443 @property
444 def encoding(self):
445 return self._encoding
446
447 @property
448 def errors(self):
449 return self._errors
450
451 @property
452 def name(self):
453 return '<%s>' % self.tags
454
455 def isatty(self):
456 return True
457
458
459 class ESC[4;38;5;81mStdOutputFile(ESC[4;38;5;149mStdioFile):
460
461 def writable(self):
462 return True
463
464 def write(self, s):
465 if self.closed:
466 raise ValueError("write to closed file")
467 s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors)
468 return self.shell.write(s, self.tags)
469
470
471 class ESC[4;38;5;81mStdInputFile(ESC[4;38;5;149mStdioFile):
472 _line_buffer = ''
473
474 def readable(self):
475 return True
476
477 def read(self, size=-1):
478 if self.closed:
479 raise ValueError("read from closed file")
480 if size is None:
481 size = -1
482 elif not isinstance(size, int):
483 raise TypeError('must be int, not ' + type(size).__name__)
484 result = self._line_buffer
485 self._line_buffer = ''
486 if size < 0:
487 while line := self.shell.readline():
488 result += line
489 else:
490 while len(result) < size:
491 line = self.shell.readline()
492 if not line: break
493 result += line
494 self._line_buffer = result[size:]
495 result = result[:size]
496 return result
497
498 def readline(self, size=-1):
499 if self.closed:
500 raise ValueError("read from closed file")
501 if size is None:
502 size = -1
503 elif not isinstance(size, int):
504 raise TypeError('must be int, not ' + type(size).__name__)
505 line = self._line_buffer or self.shell.readline()
506 if size < 0:
507 size = len(line)
508 eol = line.find('\n', 0, size)
509 if eol >= 0:
510 size = eol + 1
511 self._line_buffer = line[size:]
512 return line[:size]
513
514 def close(self):
515 self.shell.close()
516
517
518 class ESC[4;38;5;81mMyHandler(ESC[4;38;5;149mrpcESC[4;38;5;149m.ESC[4;38;5;149mRPCHandler):
519
520 def handle(self):
521 """Override base method"""
522 executive = Executive(self)
523 self.register("exec", executive)
524 self.console = self.get_remote_proxy("console")
525 sys.stdin = StdInputFile(self.console, "stdin",
526 iomenu.encoding, iomenu.errors)
527 sys.stdout = StdOutputFile(self.console, "stdout",
528 iomenu.encoding, iomenu.errors)
529 sys.stderr = StdOutputFile(self.console, "stderr",
530 iomenu.encoding, "backslashreplace")
531
532 sys.displayhook = rpc.displayhook
533 # page help() text to shell.
534 import pydoc # import must be done here to capture i/o binding
535 pydoc.pager = pydoc.plainpager
536
537 # Keep a reference to stdin so that it won't try to exit IDLE if
538 # sys.stdin gets changed from within IDLE's shell. See issue17838.
539 self._keep_stdin = sys.stdin
540
541 install_recursionlimit_wrappers()
542
543 self.interp = self.get_remote_proxy("interp")
544 rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
545
546 def exithook(self):
547 "override SocketIO method - wait for MainThread to shut us down"
548 time.sleep(10)
549
550 def EOFhook(self):
551 "Override SocketIO method - terminate wait on callback and exit thread"
552 global quitting
553 quitting = True
554 thread.interrupt_main()
555
556 def decode_interrupthook(self):
557 "interrupt awakened thread"
558 global quitting
559 quitting = True
560 thread.interrupt_main()
561
562
563 class ESC[4;38;5;81mExecutive:
564
565 def __init__(self, rpchandler):
566 self.rpchandler = rpchandler
567 if idlelib.testing is False:
568 self.locals = __main__.__dict__
569 self.calltip = calltip.Calltip()
570 self.autocomplete = autocomplete.AutoComplete()
571 else:
572 self.locals = {}
573
574 def runcode(self, code):
575 global interruptable
576 try:
577 self.user_exc_info = None
578 interruptable = True
579 try:
580 exec(code, self.locals)
581 finally:
582 interruptable = False
583 except SystemExit as e:
584 if e.args: # SystemExit called with an argument.
585 ob = e.args[0]
586 if not isinstance(ob, (type(None), int)):
587 print('SystemExit: ' + str(ob), file=sys.stderr)
588 # Return to the interactive prompt.
589 except:
590 self.user_exc_info = sys.exc_info() # For testing, hook, viewer.
591 if quitting:
592 exit()
593 if sys.excepthook is sys.__excepthook__:
594 print_exception()
595 else:
596 try:
597 sys.excepthook(*self.user_exc_info)
598 except:
599 self.user_exc_info = sys.exc_info() # For testing.
600 print_exception()
601 jit = self.rpchandler.console.getvar("<<toggle-jit-stack-viewer>>")
602 if jit:
603 self.rpchandler.interp.open_remote_stack_viewer()
604 else:
605 flush_stdout()
606
607 def interrupt_the_server(self):
608 if interruptable:
609 thread.interrupt_main()
610
611 def start_the_debugger(self, gui_adap_oid):
612 return debugger_r.start_debugger(self.rpchandler, gui_adap_oid)
613
614 def stop_the_debugger(self, idb_adap_oid):
615 "Unregister the Idb Adapter. Link objects and Idb then subject to GC"
616 self.rpchandler.unregister(idb_adap_oid)
617
618 def get_the_calltip(self, name):
619 return self.calltip.fetch_tip(name)
620
621 def get_the_completion_list(self, what, mode):
622 return self.autocomplete.fetch_completions(what, mode)
623
624 def stackviewer(self, flist_oid=None):
625 if self.user_exc_info:
626 _, exc, tb = self.user_exc_info
627 else:
628 return None
629 flist = None
630 if flist_oid is not None:
631 flist = self.rpchandler.get_remote_proxy(flist_oid)
632 while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]:
633 tb = tb.tb_next
634 exc.__traceback__ = tb
635 item = stackviewer.StackTreeItem(exc, flist)
636 return debugobj_r.remote_object_tree_item(item)
637
638
639 if __name__ == '__main__':
640 from unittest import main
641 main('idlelib.idle_test.test_run', verbosity=2)
642
643 capture_warnings(False) # Make sure turned off; see bpo-18081.