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
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