##// END OF EJS Templates
Backport PR #12141: Run ipdb in separate thread (required when using within asyncio applications).
Matthias Bussonnier -
Show More
@@ -1,147 +1,152 b''
1 1 import asyncio
2 2 import signal
3 3 import sys
4 import threading
4 5
5 6 from IPython.core.debugger import Pdb
6 7
7 8 from IPython.core.completer import IPCompleter
8 9 from .ptutils import IPythonPTCompleter
9 10 from .shortcuts import suspend_to_bg, cursor_in_leading_ws
10 11
11 12 from prompt_toolkit.enums import DEFAULT_BUFFER
12 13 from prompt_toolkit.filters import (Condition, has_focus, has_selection,
13 14 vi_insert_mode, emacs_insert_mode)
14 15 from prompt_toolkit.key_binding import KeyBindings
15 16 from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline
16 17 from pygments.token import Token
17 18 from prompt_toolkit.shortcuts.prompt import PromptSession
18 19 from prompt_toolkit.enums import EditingMode
19 20 from prompt_toolkit.formatted_text import PygmentsTokens
20 21
21 22 from prompt_toolkit import __version__ as ptk_version
22 23 PTK3 = ptk_version.startswith('3.')
23 24
24 25
25 26 class TerminalPdb(Pdb):
26 27 """Standalone IPython debugger."""
27 28
28 29 def __init__(self, *args, **kwargs):
29 30 Pdb.__init__(self, *args, **kwargs)
30 31 self._ptcomp = None
31 32 self.pt_init()
32 33
33 34 def pt_init(self):
34 35 def get_prompt_tokens():
35 36 return [(Token.Prompt, self.prompt)]
36 37
37 38 if self._ptcomp is None:
38 39 compl = IPCompleter(shell=self.shell,
39 40 namespace={},
40 41 global_namespace={},
41 42 parent=self.shell,
42 43 )
43 44 self._ptcomp = IPythonPTCompleter(compl)
44 45
45 46 kb = KeyBindings()
46 47 supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP'))
47 48 kb.add('c-z', filter=supports_suspend)(suspend_to_bg)
48 49
49 50 if self.shell.display_completions == 'readlinelike':
50 51 kb.add('tab', filter=(has_focus(DEFAULT_BUFFER)
51 52 & ~has_selection
52 53 & vi_insert_mode | emacs_insert_mode
53 54 & ~cursor_in_leading_ws
54 55 ))(display_completions_like_readline)
55 56
56 57 options = dict(
57 58 message=(lambda: PygmentsTokens(get_prompt_tokens())),
58 59 editing_mode=getattr(EditingMode, self.shell.editing_mode.upper()),
59 60 key_bindings=kb,
60 61 history=self.shell.debugger_history,
61 62 completer=self._ptcomp,
62 63 enable_history_search=True,
63 64 mouse_support=self.shell.mouse_support,
64 65 complete_style=self.shell.pt_complete_style,
65 66 style=self.shell.style,
66 67 color_depth=self.shell.color_depth,
67 68 )
68 69
69 70 if not PTK3:
70 71 options['inputhook'] = self.shell.inputhook
71 72 self.pt_loop = asyncio.new_event_loop()
72 73 self.pt_app = PromptSession(**options)
73 74
74 75 def cmdloop(self, intro=None):
75 76 """Repeatedly issue a prompt, accept input, parse an initial prefix
76 77 off the received input, and dispatch to action methods, passing them
77 78 the remainder of the line as argument.
78 79
79 80 override the same methods from cmd.Cmd to provide prompt toolkit replacement.
80 81 """
81 82 if not self.use_rawinput:
82 83 raise ValueError('Sorry ipdb does not support use_rawinput=False')
83 84
84 # In order to make sure that asyncio code written in the
85 # interactive shell doesn't interfere with the prompt, we run the
86 # prompt in a different event loop.
87 # If we don't do this, people could spawn coroutine with a
88 # while/true inside which will freeze the prompt.
89
90 try:
91 old_loop = asyncio.get_event_loop()
92 except RuntimeError:
93 # This happens when the user used `asyncio.run()`.
94 old_loop = None
95
85 # In order to make sure that prompt, which uses asyncio doesn't
86 # interfere with applications in which it's used, we always run the
87 # prompt itself in a different thread (we can't start an event loop
88 # within an event loop). This new thread won't have any event loop
89 # running, and here we run our prompt-loop.
96 90
97 91 self.preloop()
98 92
99 93 try:
100 94 if intro is not None:
101 95 self.intro = intro
102 96 if self.intro:
103 97 self.stdout.write(str(self.intro)+"\n")
104 98 stop = None
105 99 while not stop:
106 100 if self.cmdqueue:
107 101 line = self.cmdqueue.pop(0)
108 102 else:
109 103 self._ptcomp.ipy_completer.namespace = self.curframe_locals
110 104 self._ptcomp.ipy_completer.global_namespace = self.curframe.f_globals
111 105
112 asyncio.set_event_loop(self.pt_loop)
113 try:
114 line = self.pt_app.prompt()
115 except EOFError:
116 line = 'EOF'
117 finally:
118 # Restore the original event loop.
119 asyncio.set_event_loop(old_loop)
106 # Run the prompt in a different thread.
107 line = ''
108 keyboard_interrupt = False
109
110 def in_thread():
111 nonlocal line, keyboard_interrupt
112 try:
113 line = self.pt_app.prompt()
114 except EOFError:
115 line = 'EOF'
116 except KeyboardInterrupt:
117 keyboard_interrupt = True
118
119 th = threading.Thread(target=in_thread)
120 th.start()
121 th.join()
122
123 if keyboard_interrupt:
124 raise KeyboardInterrupt
120 125
121 126 line = self.precmd(line)
122 127 stop = self.onecmd(line)
123 128 stop = self.postcmd(stop, line)
124 129 self.postloop()
125 130 except Exception:
126 131 raise
127 132
128 133
129 134 def set_trace(frame=None):
130 135 """
131 136 Start debugging from `frame`.
132 137
133 138 If frame is not specified, debugging starts from caller's frame.
134 139 """
135 140 TerminalPdb().set_trace(frame or sys._getframe().f_back)
136 141
137 142
138 143 if __name__ == '__main__':
139 144 import pdb
140 145 # IPython.core.debugger.Pdb.trace_dispatch shall not catch
141 146 # bdb.BdbQuit. When started through __main__ and an exception
142 147 # happened after hitting "c", this is needed in order to
143 148 # be able to quit the debugging session (see #9950).
144 149 old_trace_dispatch = pdb.Pdb.trace_dispatch
145 150 pdb.Pdb = TerminalPdb
146 151 pdb.Pdb.trace_dispatch = old_trace_dispatch
147 152 pdb.main()
General Comments 0
You need to be logged in to leave comments. Login now