-
Notifications
You must be signed in to change notification settings - Fork 44
/
__init__.py
339 lines (294 loc) · 11.3 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# Author: Roman Miroshnychenko aka Roman V.M.
# E-mail: roman1972@gmail.com
#
# Copyright (c) 2016 Roman Miroshnychenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
A web-interface for Python's built-in PDB debugger
"""
import inspect
import os
import random
import sys
import traceback
from contextlib import contextmanager
from pdb import Pdb
from pprint import pformat
from .web_console import WebConsole
__all__ = ['WebPdb', 'set_trace', 'post_mortem', 'catch_post_mortem']
class WebPdb(Pdb):
"""
The main debugger class
It provides a web-interface for Python's built-in PDB debugger
with extra convenience features.
"""
active_instance = None
null = object()
def __init__(self, host='', port=5555, patch_stdstreams=False):
"""
:param host: web-UI hostname or IP-address
:type host: str
:param port: web-UI port. If ``port=-1``, choose a random port value
between 32768 and 65536.
:type port: int
:param patch_stdstreams: redirect all standard input and output
streams to the web-UI.
:type patch_stdstreams: bool
"""
if port == -1:
random.seed()
port = random.randint(32768, 65536)
self.console = WebConsole(host, port, self)
super().__init__(stdin=self.console, stdout=self.console)
# Borrowed from here: https://github.com/ionelmc/python-remote-pdb
self._backup = []
if patch_stdstreams:
for name in (
'stderr',
'stdout',
'__stderr__',
'__stdout__',
'stdin',
'__stdin__',
):
self._backup.append((name, getattr(sys, name)))
setattr(sys, name, self.console)
WebPdb.active_instance = self
def do_quit(self, arg):
"""
quit || exit || q
Stop and quit the current debugging session
"""
for name, fh in self._backup:
setattr(sys, name, fh)
self.console.writeline('*** Aborting program ***\n')
self.console.flush()
self.console.close()
WebPdb.active_instance = None
return super().do_quit(arg)
do_q = do_exit = do_quit
def do_inspect(self, arg):
"""
i(nspect) object
Inspect an object
"""
if arg in self.curframe_locals:
obj = self.curframe_locals[arg]
elif arg in self.curframe.f_globals:
obj = self.curframe.f_globals[arg]
else:
obj = WebPdb.null
if obj is not WebPdb.null:
self.console.writeline(f'{arg} = {type(obj)}:\n')
for name, value in inspect.getmembers(obj):
if not (name.startswith('__') and (name.endswith('__'))):
repr_value = self._get_repr(value, pretty=True, indent=8)
self.console.writeline(f' {name}: {repr_value}\n')
else:
self.console.writeline(f'NameError: name "{arg}" is not defined\n')
self.console.flush()
do_i = do_inspect
@staticmethod
def _get_repr(obj, pretty=False, indent=1):
"""
Get string representation of an object
:param obj: object
:type obj: object
:param pretty: use pretty formatting
:type pretty: bool
:param indent: indentation for pretty formatting
:type indent: int
:return: string representation
:rtype: str
"""
if pretty:
repr_value = pformat(obj, indent)
else:
repr_value = repr(obj)
return repr_value
def set_continue(self):
# We do not detach the debugger
# for correct multiple set_trace() and post_mortem() calls.
self._set_stopinfo(self.botframe, None, -1)
def dispatch_return(self, frame, arg):
# The parent's method needs to be called first.
ret = super().dispatch_return(frame, arg)
if frame.f_back is None:
self.console.writeline('*** Thread finished ***\n')
if not self.console.closed:
self.console.flush()
self.console.close()
WebPdb.active_instance = None
return ret
def get_current_frame_data(self):
"""
Get all date about the current execution frame
:return: current frame data
:rtype: dict
:raises AttributeError: if the debugger does hold any execution frame.
:raises IOError: if source code for the current execution frame is not accessible.
"""
filename = self.curframe.f_code.co_filename
lines, _ = inspect.findsource(self.curframe)
return {
'dirname': os.path.dirname(os.path.abspath(filename)) + os.path.sep,
'filename': os.path.basename(filename),
'file_listing': ''.join(lines),
'current_line': self.curframe.f_lineno,
'breakpoints': self.get_file_breaks(filename),
'globals': self.get_globals(),
'locals': self.get_locals()
}
def _format_variables(self, raw_vars):
"""
:param raw_vars: a `dict` of `var_name: var_object` pairs
:type raw_vars: dict
:return: sorted list of variables as a unicode string
:rtype: unicode
"""
f_vars = []
for var, value in raw_vars.items():
if not (var.startswith('__') and var.endswith('__')):
repr_value = self._get_repr(value)
f_vars.append(f'{var} = {repr_value}')
return '\n'.join(sorted(f_vars))
def get_globals(self):
"""
Get the listing of global variables in the current scope
.. note:: special variables that start and end with
double underscores ``__`` are not included.
:return: a listing of ``var = value`` pairs sorted alphabetically
:rtype: unicode
"""
return self._format_variables(self.curframe.f_globals)
def get_locals(self):
"""
Get the listing of local variables in the current scope
.. note:: special variables that start and end with
double underscores ``__`` are not included.
For module scope globals and locals listings are the same.
:return: a listing of ``var = value`` pairs sorted alphabetically
:rtype: unicode
"""
return self._format_variables(self.curframe_locals)
def remove_trace(self, frame=None):
"""
Detach the debugger from the execution stack
:param frame: the lowest frame to detach the debugger from.
:type frame: types.FrameType
"""
sys.settrace(None)
if frame is None:
frame = self.curframe
while frame and frame is not self.botframe:
del frame.f_trace
frame = frame.f_back
def set_trace(host='', port=5555, patch_stdstreams=False):
"""
Start the debugger
This method suspends execution of the current script
and starts a PDB debugging session. The web-interface is opened
on the specified port (default: ``5555``).
Example::
import web_pdb;web_pdb.set_trace()
Subsequent :func:`set_trace` calls can be used as hardcoded breakpoints.
:param host: web-UI hostname or IP-address
:type host: str
:param port: web-UI port. If ``port=-1``, choose a random port value
between 32768 and 65536.
:type port: int
:param patch_stdstreams: redirect all standard input and output
streams to the web-UI.
:type patch_stdstreams: bool
"""
pdb = WebPdb.active_instance
if pdb is None:
pdb = WebPdb(host, port, patch_stdstreams)
else:
# If the debugger is still attached reset trace to a new location
pdb.remove_trace()
pdb.set_trace(sys._getframe().f_back) # pylint: disable=protected-access
def post_mortem(tb=None, host='', port=5555, patch_stdstreams=False):
"""
Start post-mortem debugging for the provided traceback object
If no traceback is provided the debugger tries to obtain a traceback
for the last unhandled exception.
Example::
try:
# Some error-prone code
assert ham == spam
except:
web_pdb.post_mortem()
:param tb: traceback for post-mortem debugging
:type tb: types.TracebackType
:param host: web-UI hostname or IP-address
:type host: str
:param port: web-UI port. If ``port=-1``, choose a random port value
between 32768 and 65536.
:type port: int
:param patch_stdstreams: redirect all standard input and output
streams to the web-UI.
:type patch_stdstreams: bool
:raises ValueError: if no valid traceback is provided and the Python
interpreter is not handling any exception
"""
# handling the default
if tb is None:
# sys.exc_info() returns (type, value, traceback) if an exception is
# being handled, otherwise it returns (None, None, None)
t, v, tb = sys.exc_info()
exc_data = traceback.format_exception(t, v, tb)
else:
exc_data = traceback.format_tb(tb)
if tb is None:
raise ValueError('A valid traceback must be passed if no '
'exception is being handled')
pdb = WebPdb.active_instance
if pdb is None:
pdb = WebPdb(host, port, patch_stdstreams)
else:
pdb.remove_trace()
pdb.console.writeline('*** Web-PDB post-mortem ***\n')
pdb.console.writeline(''.join(exc_data))
pdb.reset()
pdb.interaction(None, tb)
@contextmanager
def catch_post_mortem(host='', port=5555, patch_stdstreams=False):
"""
A context manager for tracking potentially error-prone code
If an unhandled exception is raised inside context manager's code block,
the post-mortem debugger is started automatically.
Example::
with web_pdb.catch_post_mortem()
# Some error-prone code
assert ham == spam
:param host: web-UI hostname or IP-address
:type host: str
:param port: web-UI port. If ``port=-1``, choose a random port value
between 32768 and 65536.
:type port: int
:param patch_stdstreams: redirect all standard input and output
streams to the web-UI.
:type patch_stdstreams: bool
"""
try:
yield
except Exception: # pylint: disable=broad-except
post_mortem(None, host, port, patch_stdstreams)