wrapper around _pyrepl (basically: code.interact but w/ colorization and tab-completions) that mitigates its side-effects and patches in the command-style (no-parentheses) "exit" syntax to closely emulate the true modern python repl experience
pywrapl.py
189 lines 7.0 kB view raw
1 2import sys 3 4_VERSION = { 5 "major": [3], 6 "minor": [14], 7 "micro": [2] 8} 9 10class VersionMismatch(RuntimeError): 11 pass 12 13class _ReplExitSentinel: 14 pass 15 16def _wrapped_repl(local, log, on_version_mismatch="error"): 17 """ 18 Wraps a `_pyrepl` console to (attempt to) ensure it doesn't make a mess of any global state. 19 20 Most important is putting the original `builtins.input` back in place, as pyrepl neglects 21 to do. 22 """ 23 24 import builtins 25 import signal 26 import threading 27 import linecache 28 from _pyrepl import console, simple_interact, readline, trace, historical_reader 29 30 version = sys.version_info 31 if not (version.major in _VERSION["major"] and version.minor in _VERSION["minor"] and version.micro in _VERSION["micro"]): 32 if on_version_mismatch == "error": 33 raise VersionMismatch("wrapper for unsupported _pyrepl module was designed around a different version") 34 elif on_version_mismatch == "warning": 35 log("wrapper for unsupported _pyrepl module was designed around a different version", mode="warning") 36 elif on_version_mismatch == "ignore": 37 log("on_version_mismatch should be one of [\"error\", \"warning\", \"ignore\"]", mode="warning") 38 39 local["exit"] = _ReplExitSentinel() 40 local["quit"] = _ReplExitSentinel() 41 42 def save_attr(module, name): 43 if hasattr(module, name): 44 return (True, getattr(module, name)) 45 return (False, None) 46 47 original_state = { 48 # changed by readline._setup 49 "builtins.input": builtins.input, 50 51 "sys.ps1": save_attr(sys, "ps1"), 52 "sys.ps2": save_attr(sys, "ps2"), 53 54 # signal handlers (only on platforms where they exist) 55 "signal.SIGCONT": (hasattr(signal, "SIGCONT") and save_attr(signal, "SIGCONT")[1]) or None, 56 "signal.SIGWINCH": (hasattr(signal, "SIGWINCH") and save_attr(signal, "SIGWINCH")[1]) or None, 57 58 "threading.excepthook": save_attr(threading, "excepthook"), 59 60 "historical_reader.should_auto_add_history": historical_reader.should_auto_add_history, 61 62 "trace.trace_file": trace.trace_file, # may be None or a file object 63 } 64 65 readline_wrapper = getattr(readline, "_wrapper", None) 66 original_readline = { 67 "wrapper": readline_wrapper, 68 "history": (readline_wrapper.get_reader().history[:] 69 if readline_wrapper and readline_wrapper.reader else []), 70 "module_completer": (readline_wrapper.config.module_completer 71 if readline_wrapper else None), 72 } 73 74 try: 75 repl = console.InteractiveColoredConsole(locals=local, local_exit=True) 76 simple_interact.run_multiline_interactive_console(repl) 77 except SystemExit: 78 pass 79 finally: 80 builtins.input = original_state["builtins.input"] 81 82 for name in ("ps1", "ps2"): 83 existed, value = original_state[f"sys.{name}"] 84 if existed: 85 sys.__dict__[name] = value 86 else: 87 sys.__dict__.pop(name, None) 88 89 for sig_name, orig_handler in [("SIGCONT", original_state["signal.SIGCONT"]), 90 ("SIGWINCH", original_state["signal.SIGWINCH"])]: 91 if hasattr(signal, sig_name): 92 sig = getattr(signal, sig_name) 93 current = signal.getsignal(sig) 94 if current != orig_handler: 95 try: 96 signal.signal(sig, orig_handler) 97 except TypeError: 98 # not a problem. happens when orig_handler is None 99 pass 100 except ValueError: 101 # TODO: when does this happen? are other errors possible? 102 pass 103 104 existed, value = original_state["threading.excepthook"] 105 if existed: 106 threading.excepthook = value 107 else: 108 if hasattr(threading, "excepthook"): 109 delattr(threading, "excepthook") 110 111 historical_reader.should_auto_add_history = original_state["historical_reader.should_auto_add_history"] 112 113 # close trace file if _pyrepl opened a new one 114 orig_trace_file = original_state["trace.trace_file"] 115 current_trace_file = trace.trace_file 116 if current_trace_file is not None and current_trace_file is not orig_trace_file: 117 try: 118 current_trace_file.close() 119 except Exception: 120 log("failed to close trace file opened by _pyrepl. might be fine", mode="warning") 121 trace.trace_file = orig_trace_file # may be None 122 123 prefixes = ('<python-input', '<stdin>', '<input>', '<console>') 124 try: 125 for filename in list(linecache.cache.keys()): 126 if filename.startswith(prefixes): 127 del linecache.cache[filename] 128 except Exception: 129 log.trace() 130 log("ran into an issue cleaning up _pyrepl's linecache changes. might be fine", mode="warning") 131 132 try: 133 readline.clear_history() 134 for line in original_readline["history"]: 135 readline.add_history(line) 136 if readline_wrapper and original_readline["module_completer"] is not None: 137 readline_wrapper.config.module_completer = original_readline["module_completer"] 138 except Exception: 139 log.trace() 140 log("ran into an issue cleaning up _pyrepl's history/completer changes. might be fine", mode="warning") 141 142 if readline_wrapper and hasattr(readline_wrapper, 'reader') and readline_wrapper.reader: 143 try: 144 reader = readline_wrapper.reader 145 146 reader.buffer = [] 147 reader.pos = 0 148 reader.historyi = len(reader.history) # point to "new" entry 149 reader.transient_history = {} 150 reader.dirty = True # force refresh if reused 151 152 reader.restore() 153 except Exception: 154 log.trace() 155 log("ran into an issue resetting the readline_wrapper reader. might be fine", mode="warning") 156 157 158 159def repl(local, log, on_version_mismatch="error"): 160 """ 161 Wrapped _pyrepl with a fallback to code.interact 162 """ 163 164 exit_repl = _ReplExitSentinel() 165 local["exit"] = exit_repl 166 local["quit"] = exit_repl 167 168 old_hook = sys.displayhook 169 170 def _wrapped_hook(value): 171 if isinstance(value, _ReplExitSentinel): 172 raise SystemExit 173 else: 174 return old_hook(value) 175 176 try: 177 sys.displayhook = _wrapped_hook 178 179 _wrapped_repl(local, log, on_version_mismatch) 180 181 except: 182 # Catch everything bc there's no long-term guarantees about how _pyrepl works 183 log.trace() 184 log("failed to launch fancy colorful python repl :( here's a boring one:", mode="warning") 185 import code 186 code.interact(local=local, banner="", exitmsg="", local_exit=True) 187 finally: 188 sys.displayhook = old_hook 189