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