pyflyby 1.10.1__cp311-cp311-manylinux_2_24_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyflyby might be problematic. Click here for more details.
- pyflyby/__init__.py +61 -0
- pyflyby/__main__.py +9 -0
- pyflyby/_autoimp.py +2229 -0
- pyflyby/_cmdline.py +548 -0
- pyflyby/_comms.py +221 -0
- pyflyby/_dbg.py +1367 -0
- pyflyby/_docxref.py +379 -0
- pyflyby/_dynimp.py +154 -0
- pyflyby/_fast_iter_modules.cpython-311-x86_64-linux-gnu.so +0 -0
- pyflyby/_file.py +771 -0
- pyflyby/_flags.py +230 -0
- pyflyby/_format.py +186 -0
- pyflyby/_idents.py +227 -0
- pyflyby/_import_sorting.py +165 -0
- pyflyby/_importclns.py +658 -0
- pyflyby/_importdb.py +680 -0
- pyflyby/_imports2s.py +643 -0
- pyflyby/_importstmt.py +723 -0
- pyflyby/_interactive.py +2113 -0
- pyflyby/_livepatch.py +793 -0
- pyflyby/_log.py +104 -0
- pyflyby/_modules.py +641 -0
- pyflyby/_parse.py +1381 -0
- pyflyby/_py.py +2166 -0
- pyflyby/_saveframe.py +1145 -0
- pyflyby/_saveframe_reader.py +471 -0
- pyflyby/_util.py +458 -0
- pyflyby/_version.py +7 -0
- pyflyby/autoimport.py +20 -0
- pyflyby/etc/pyflyby/canonical.py +10 -0
- pyflyby/etc/pyflyby/common.py +27 -0
- pyflyby/etc/pyflyby/forget.py +10 -0
- pyflyby/etc/pyflyby/mandatory.py +10 -0
- pyflyby/etc/pyflyby/numpy.py +156 -0
- pyflyby/etc/pyflyby/std.py +335 -0
- pyflyby/importdb.py +19 -0
- pyflyby/libexec/pyflyby/colordiff +34 -0
- pyflyby/libexec/pyflyby/diff-colorize +148 -0
- pyflyby/share/emacs/site-lisp/pyflyby.el +108 -0
- pyflyby-1.10.1.data/scripts/collect-exports +76 -0
- pyflyby-1.10.1.data/scripts/collect-imports +58 -0
- pyflyby-1.10.1.data/scripts/find-import +38 -0
- pyflyby-1.10.1.data/scripts/list-bad-xrefs +34 -0
- pyflyby-1.10.1.data/scripts/prune-broken-imports +34 -0
- pyflyby-1.10.1.data/scripts/pyflyby-diff +34 -0
- pyflyby-1.10.1.data/scripts/reformat-imports +27 -0
- pyflyby-1.10.1.data/scripts/replace-star-imports +37 -0
- pyflyby-1.10.1.data/scripts/saveframe +299 -0
- pyflyby-1.10.1.data/scripts/tidy-imports +163 -0
- pyflyby-1.10.1.data/scripts/transform-imports +47 -0
- pyflyby-1.10.1.dist-info/METADATA +591 -0
- pyflyby-1.10.1.dist-info/RECORD +55 -0
- pyflyby-1.10.1.dist-info/WHEEL +5 -0
- pyflyby-1.10.1.dist-info/entry_points.txt +4 -0
- pyflyby-1.10.1.dist-info/licenses/LICENSE.txt +23 -0
pyflyby/_saveframe.py
ADDED
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyflyby/_saveframe.py
|
|
3
|
+
|
|
4
|
+
Provides a utility to save the info for debugging / reproducing any issue.
|
|
5
|
+
Checkout the doc of the `saveframe` function for more info.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
import inspect
|
|
14
|
+
import keyword
|
|
15
|
+
import linecache
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import pickle
|
|
19
|
+
import re
|
|
20
|
+
import stat
|
|
21
|
+
import sys
|
|
22
|
+
import traceback
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
The protocol used while pickling the frame's data.
|
|
26
|
+
"""
|
|
27
|
+
PICKLE_PROTOCOL=5
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
The permissions used to create the file where the data is stored by the
|
|
31
|
+
'saveframe' utility.
|
|
32
|
+
"""
|
|
33
|
+
FILE_PERMISSION = 0o644
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
The default filename used for storing the data when the user does not explicitly
|
|
37
|
+
provide one.
|
|
38
|
+
"""
|
|
39
|
+
DEFAULT_FILENAME = 'saveframe.pkl'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ExceptionInfo:
|
|
44
|
+
"""
|
|
45
|
+
A dataclass to store the exception info.
|
|
46
|
+
"""
|
|
47
|
+
exception_string: str
|
|
48
|
+
exception_full_string: str
|
|
49
|
+
exception_class_name: str
|
|
50
|
+
exception_class_qualname: str
|
|
51
|
+
exception_object: object
|
|
52
|
+
traceback: list
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class FrameMetadata:
|
|
57
|
+
"""
|
|
58
|
+
A dataclass to store a frame's metadata.
|
|
59
|
+
"""
|
|
60
|
+
frame_index: int
|
|
61
|
+
filename: str
|
|
62
|
+
lineno: int
|
|
63
|
+
function_name: str
|
|
64
|
+
function_qualname: str
|
|
65
|
+
function_object: bytes
|
|
66
|
+
module_name: str
|
|
67
|
+
code: str
|
|
68
|
+
frame_identifier: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FrameFormat(Enum):
|
|
72
|
+
"""
|
|
73
|
+
Enum class to store the different formats supported by the `frames` argument
|
|
74
|
+
in the `saveframe` utility. See the doc of `saveframe` for more info.
|
|
75
|
+
"""
|
|
76
|
+
NUM = "NUM"
|
|
77
|
+
LIST = "LIST"
|
|
78
|
+
RANGE = "RANGE"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_saveframe_logger():
|
|
82
|
+
"""
|
|
83
|
+
Get the logger used for the saveframe utility.
|
|
84
|
+
"""
|
|
85
|
+
log_format = (
|
|
86
|
+
"[%(asctime)s:%(msecs)03d pyflyby.saveframe:%(lineno)s "
|
|
87
|
+
"%(levelname)s] %(message)s")
|
|
88
|
+
log_datefmt = "%Y%m%d %H:%M:%S"
|
|
89
|
+
logger = logging.getLogger(__name__)
|
|
90
|
+
logger.setLevel(logging.INFO)
|
|
91
|
+
if not logger.handlers:
|
|
92
|
+
stream_handler = logging.StreamHandler()
|
|
93
|
+
stream_handler.setLevel(logging.INFO)
|
|
94
|
+
formatter = logging.Formatter(
|
|
95
|
+
fmt=log_format, datefmt=log_datefmt)
|
|
96
|
+
stream_handler.setFormatter(formatter)
|
|
97
|
+
logger.addHandler(stream_handler)
|
|
98
|
+
return logger
|
|
99
|
+
|
|
100
|
+
_SAVEFRAME_LOGGER = _get_saveframe_logger()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@contextmanager
|
|
104
|
+
def _open_file(filename, mode):
|
|
105
|
+
"""
|
|
106
|
+
A context manager to open the ``filename`` with ``mode``.
|
|
107
|
+
This function ignores the ``umask`` while creating the file.
|
|
108
|
+
|
|
109
|
+
:param filename:
|
|
110
|
+
The file to open.
|
|
111
|
+
:param mode:
|
|
112
|
+
Mode in which to open the file.
|
|
113
|
+
"""
|
|
114
|
+
old_umask = os.umask(0)
|
|
115
|
+
fd = None
|
|
116
|
+
file_obj = None
|
|
117
|
+
try:
|
|
118
|
+
fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, FILE_PERMISSION)
|
|
119
|
+
file_obj = os.fdopen(fd, mode)
|
|
120
|
+
yield file_obj
|
|
121
|
+
finally:
|
|
122
|
+
if file_obj is not None:
|
|
123
|
+
file_obj.close()
|
|
124
|
+
elif fd is not None:
|
|
125
|
+
os.close(fd)
|
|
126
|
+
os.umask(old_umask)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _get_exception_info(exception_obj):
|
|
130
|
+
"""
|
|
131
|
+
Get the metadata information for the ``exception_obj``.
|
|
132
|
+
|
|
133
|
+
:param exception_obj:
|
|
134
|
+
The exception raised by the user's code.
|
|
135
|
+
:return:
|
|
136
|
+
An `ExceptionInfo` object.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
tb = (
|
|
140
|
+
traceback.format_exception(
|
|
141
|
+
type(exception_obj), exception_obj, exception_obj.__traceback__))
|
|
142
|
+
except Exception as err:
|
|
143
|
+
_SAVEFRAME_LOGGER.warning(
|
|
144
|
+
"Error while formatting the traceback. Error: %a", err)
|
|
145
|
+
tb = "Traceback couldn't be formatted"
|
|
146
|
+
exception_info = ExceptionInfo(
|
|
147
|
+
exception_string=str(exception_obj),
|
|
148
|
+
exception_full_string=f'{exception_obj.__class__.__name__}: {exception_obj}',
|
|
149
|
+
exception_class_name=exception_obj.__class__.__name__,
|
|
150
|
+
exception_class_qualname=exception_obj.__class__.__qualname__,
|
|
151
|
+
exception_object=exception_obj,
|
|
152
|
+
traceback=tb
|
|
153
|
+
)
|
|
154
|
+
return exception_info
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _get_qualname(frame):
|
|
158
|
+
"""
|
|
159
|
+
Get fully qualified name of the function for the ``frame``.
|
|
160
|
+
|
|
161
|
+
In python 3.10, ``co_qualname`` attribute is not present, so use ``co_name``.
|
|
162
|
+
"""
|
|
163
|
+
return (frame.f_code.co_qualname if hasattr(frame.f_code, "co_qualname")
|
|
164
|
+
else frame.f_code.co_name)
|
|
165
|
+
|
|
166
|
+
def _get_frame_repr(frame):
|
|
167
|
+
"""
|
|
168
|
+
Construct repr for the ``frame``. This is used in the info messages.
|
|
169
|
+
|
|
170
|
+
:param frame:
|
|
171
|
+
The frame object.
|
|
172
|
+
:return:
|
|
173
|
+
The string f'File: {filename}, Line: {lineno}, Function: {function_qualname}'
|
|
174
|
+
"""
|
|
175
|
+
return (f"'File: {frame.f_code.co_filename}, Line: {frame.f_lineno}, "
|
|
176
|
+
f"Function: {_get_qualname(frame)}'")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _get_frame_local_variables_data(frame, variables, exclude_variables):
|
|
180
|
+
"""
|
|
181
|
+
Get the local variables data of the ``frame``.
|
|
182
|
+
|
|
183
|
+
:param frame:
|
|
184
|
+
The frame object
|
|
185
|
+
:param variables:
|
|
186
|
+
Local variables to be included.
|
|
187
|
+
:param exclude_variables:
|
|
188
|
+
Local variables to be excluded.
|
|
189
|
+
:return:
|
|
190
|
+
A dict containing the local variables data, with the key as the variable
|
|
191
|
+
name and the value as the pickled local variable value.
|
|
192
|
+
"""
|
|
193
|
+
# A dict to store the local variables to be saved.
|
|
194
|
+
local_variables_to_save = {}
|
|
195
|
+
all_local_variables = frame.f_locals
|
|
196
|
+
for variable in all_local_variables:
|
|
197
|
+
# Discard the variables that starts with '__' like '__eq__', etc., to
|
|
198
|
+
# keep the data clean.
|
|
199
|
+
if variable.startswith('__'):
|
|
200
|
+
continue
|
|
201
|
+
if variables and variable not in variables:
|
|
202
|
+
continue
|
|
203
|
+
if exclude_variables and variable in exclude_variables:
|
|
204
|
+
continue
|
|
205
|
+
try:
|
|
206
|
+
pickled_value = pickle.dumps(
|
|
207
|
+
all_local_variables[variable], protocol=PICKLE_PROTOCOL)
|
|
208
|
+
except Exception as err:
|
|
209
|
+
_SAVEFRAME_LOGGER.warning(
|
|
210
|
+
"Cannot pickle variable: %a for frame: %s. Error: %a. Skipping "
|
|
211
|
+
"this variable and continuing.",
|
|
212
|
+
variable, _get_frame_repr(frame), err)
|
|
213
|
+
else:
|
|
214
|
+
local_variables_to_save[variable] = pickled_value
|
|
215
|
+
return local_variables_to_save
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _get_frame_function_object(frame):
|
|
219
|
+
"""
|
|
220
|
+
Get the function object of the frame.
|
|
221
|
+
|
|
222
|
+
This helper does a best-effort attempt to find the function object using
|
|
223
|
+
locals and globals dict.
|
|
224
|
+
|
|
225
|
+
:param frame:
|
|
226
|
+
The frame object.
|
|
227
|
+
:return:
|
|
228
|
+
The function object from which the ``frame`` is originating.
|
|
229
|
+
"""
|
|
230
|
+
func_name = frame.f_code.co_name
|
|
231
|
+
func_qualname = _get_qualname(frame)
|
|
232
|
+
info_msg = f"Can't get function object for frame: {_get_frame_repr(frame)}"
|
|
233
|
+
return_msg = "Function object not found"
|
|
234
|
+
# The function is most-likely either a local function or a class method.
|
|
235
|
+
if func_name != func_qualname:
|
|
236
|
+
prev_frame = frame.f_back
|
|
237
|
+
# Handle the local functions.
|
|
238
|
+
if "<locals>" in func_qualname:
|
|
239
|
+
if prev_frame is None:
|
|
240
|
+
_SAVEFRAME_LOGGER.info(info_msg)
|
|
241
|
+
return return_msg
|
|
242
|
+
# The function is present in the previous frame's (the parent) locals.
|
|
243
|
+
if func_name in prev_frame.f_locals:
|
|
244
|
+
return prev_frame.f_locals[func_name]
|
|
245
|
+
_SAVEFRAME_LOGGER.info(info_msg)
|
|
246
|
+
return return_msg
|
|
247
|
+
# Handle the class methods.
|
|
248
|
+
else:
|
|
249
|
+
try:
|
|
250
|
+
func_parent = func_qualname.split('.')[-2]
|
|
251
|
+
except IndexError:
|
|
252
|
+
_SAVEFRAME_LOGGER.info(info_msg)
|
|
253
|
+
return return_msg
|
|
254
|
+
# The parent is present in the globals, so extract the function object
|
|
255
|
+
# using getattr.
|
|
256
|
+
if func_parent in frame.f_globals:
|
|
257
|
+
func_parent_obj = frame.f_globals[func_parent]
|
|
258
|
+
if hasattr(func_parent_obj, func_name):
|
|
259
|
+
return getattr(func_parent_obj, func_name)
|
|
260
|
+
_SAVEFRAME_LOGGER.info(info_msg)
|
|
261
|
+
return return_msg
|
|
262
|
+
# The function is most-likely a global function.
|
|
263
|
+
else:
|
|
264
|
+
if func_name in frame.f_globals:
|
|
265
|
+
return frame.f_globals[func_name]
|
|
266
|
+
_SAVEFRAME_LOGGER.info(info_msg)
|
|
267
|
+
return return_msg
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _get_frame_module_name(frame):
|
|
271
|
+
"""
|
|
272
|
+
Get the module name of the ``frame``.
|
|
273
|
+
|
|
274
|
+
:param frame:
|
|
275
|
+
The frame object.
|
|
276
|
+
:return:
|
|
277
|
+
The name of the module from which the ``frame`` is originating.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
frame_module = inspect.getmodule(frame)
|
|
281
|
+
if frame_module is not None:
|
|
282
|
+
return frame_module.__name__
|
|
283
|
+
_SAVEFRAME_LOGGER.info(
|
|
284
|
+
"No module found for the frame: %s", _get_frame_repr(frame))
|
|
285
|
+
return "Module name not found"
|
|
286
|
+
except Exception as err:
|
|
287
|
+
_SAVEFRAME_LOGGER.warning(
|
|
288
|
+
"Module name couldn't be found for the frame: %s. Error: %a",
|
|
289
|
+
_get_frame_repr(frame), err)
|
|
290
|
+
return "Module name not found"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _get_frame_code_line(frame):
|
|
294
|
+
"""
|
|
295
|
+
Get the code line of the ``frame``.
|
|
296
|
+
|
|
297
|
+
:param frame:
|
|
298
|
+
The frame object.
|
|
299
|
+
:return:
|
|
300
|
+
The code line as returned by the `linecache` package.
|
|
301
|
+
"""
|
|
302
|
+
filename = frame.f_code.co_filename
|
|
303
|
+
lineno = frame.f_lineno
|
|
304
|
+
code_line = linecache.getline(filename, lineno).strip()
|
|
305
|
+
if code_line is None:
|
|
306
|
+
code_line = f"No code content found at {filename!a}: {lineno}"
|
|
307
|
+
_SAVEFRAME_LOGGER.info(code_line + f" for frame {_get_frame_repr(frame)}")
|
|
308
|
+
return code_line
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _get_frame_metadata(frame_idx, frame_obj):
|
|
312
|
+
"""
|
|
313
|
+
Get metadata for the frame ``frame_obj``.
|
|
314
|
+
|
|
315
|
+
:param frame_idx:
|
|
316
|
+
Index of the frame ``frame_obj`` from the bottom of the stack trace.
|
|
317
|
+
:param frame_obj:
|
|
318
|
+
The frame object for which to get the metadata.
|
|
319
|
+
:return:
|
|
320
|
+
A `FrameMetadata` object.
|
|
321
|
+
"""
|
|
322
|
+
frame_function_object = _get_frame_function_object(frame_obj)
|
|
323
|
+
try:
|
|
324
|
+
if isinstance(frame_function_object, str):
|
|
325
|
+
# Function object couldn't be found.
|
|
326
|
+
pickled_function = frame_function_object
|
|
327
|
+
else:
|
|
328
|
+
pickled_function = pickle.dumps(
|
|
329
|
+
frame_function_object, protocol=PICKLE_PROTOCOL)
|
|
330
|
+
except Exception as err:
|
|
331
|
+
_SAVEFRAME_LOGGER.info(
|
|
332
|
+
"Cannot pickle the function object for the frame: %s. Error: %a",
|
|
333
|
+
_get_frame_repr(frame_obj), err)
|
|
334
|
+
pickled_function = "Function object not pickleable"
|
|
335
|
+
# Object that stores all the frame's metadata.
|
|
336
|
+
frame_metadata = FrameMetadata(
|
|
337
|
+
frame_index=frame_idx,
|
|
338
|
+
filename=frame_obj.f_code.co_filename,
|
|
339
|
+
lineno=frame_obj.f_lineno,
|
|
340
|
+
function_name=frame_obj.f_code.co_name,
|
|
341
|
+
function_qualname=_get_qualname(frame_obj),
|
|
342
|
+
function_object=pickled_function,
|
|
343
|
+
module_name=_get_frame_module_name(frame_obj),
|
|
344
|
+
code=_get_frame_code_line(frame_obj),
|
|
345
|
+
frame_identifier=(
|
|
346
|
+
f"{frame_obj.f_code.co_filename},{frame_obj.f_lineno},"
|
|
347
|
+
f"{frame_obj.f_code.co_name}")
|
|
348
|
+
)
|
|
349
|
+
return frame_metadata
|
|
350
|
+
|
|
351
|
+
def _get_all_matching_frames(frame, all_frames):
|
|
352
|
+
"""
|
|
353
|
+
Get all the frames from ``all_frames`` that match the ``frame``.
|
|
354
|
+
|
|
355
|
+
The matching is done based on the filename / file regex, the line number
|
|
356
|
+
and the function name.
|
|
357
|
+
|
|
358
|
+
:param frame:
|
|
359
|
+
Frame for which to find all the matching frames.
|
|
360
|
+
:param all_frames:
|
|
361
|
+
A list of all the frame objects from the exception object.
|
|
362
|
+
:return:
|
|
363
|
+
A list of all the frames that match the ``frame``. Each item in the list
|
|
364
|
+
is a tuple of 2 elements, where the first element is the frame index
|
|
365
|
+
(starting from the bottom of the stack trace) and the second element is
|
|
366
|
+
the frame object.
|
|
367
|
+
"""
|
|
368
|
+
if frame == ['']:
|
|
369
|
+
# This is the case where the last frame is not passed in the range
|
|
370
|
+
# ('first_frame..'). Return the first frame from the bottom as the last
|
|
371
|
+
# frame.
|
|
372
|
+
return [(1, all_frames[0])]
|
|
373
|
+
all_matching_frames = []
|
|
374
|
+
filename_regex, lineno, func_name = frame
|
|
375
|
+
for idx, frame_obj in enumerate(all_frames):
|
|
376
|
+
if re.search(filename_regex, frame_obj.f_code.co_filename) is None:
|
|
377
|
+
continue
|
|
378
|
+
if lineno and frame_obj.f_lineno != lineno:
|
|
379
|
+
continue
|
|
380
|
+
if (func_name and
|
|
381
|
+
func_name not in (frame_obj.f_code.co_name, _get_qualname(frame_obj))):
|
|
382
|
+
continue
|
|
383
|
+
all_matching_frames.append((idx+1, frame_obj))
|
|
384
|
+
return all_matching_frames
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _get_frames_to_save(frames, all_frames):
|
|
388
|
+
"""
|
|
389
|
+
Get the frames we want to save from ``all_frames`` as per ``frames``.
|
|
390
|
+
|
|
391
|
+
:param frames:
|
|
392
|
+
Frames that user wants to save. This parameter stores the parsed frames
|
|
393
|
+
returned by `_validate_frames` function.
|
|
394
|
+
:param all_frames:
|
|
395
|
+
A list of all the frame objects from the exception object.
|
|
396
|
+
:return:
|
|
397
|
+
A list of filtered frames to save. Each item in the list is a tuple of 2
|
|
398
|
+
elements, where the first element is the frame index (starting from the
|
|
399
|
+
bottom of the stack trace) and the second element is the frame object.
|
|
400
|
+
"""
|
|
401
|
+
frames, frame_type = frames
|
|
402
|
+
filtered_frames = []
|
|
403
|
+
if frame_type is None:
|
|
404
|
+
# No frame passed by the user, return the first frame from the bottom
|
|
405
|
+
# of the stack trace.
|
|
406
|
+
return [(1, all_frames[0])]
|
|
407
|
+
elif frame_type == FrameFormat.NUM:
|
|
408
|
+
if len(all_frames) < frames:
|
|
409
|
+
_SAVEFRAME_LOGGER.info(
|
|
410
|
+
"Number of frames to dump are %s, but there are only %s frames "
|
|
411
|
+
"in the error stack. So dumping all the frames.",
|
|
412
|
+
frames, len(all_frames))
|
|
413
|
+
frames = len(all_frames)
|
|
414
|
+
return [(idx+1, all_frames[idx]) for idx in range(frames)]
|
|
415
|
+
elif frame_type == FrameFormat.LIST:
|
|
416
|
+
for frame in frames:
|
|
417
|
+
filtered_frames.extend(_get_all_matching_frames(frame, all_frames))
|
|
418
|
+
elif frame_type == FrameFormat.RANGE:
|
|
419
|
+
# Handle 'first_frame..last_frame' and 'first_frame..' formats.
|
|
420
|
+
# Find all the matching frames for the first_frame and last_frame.
|
|
421
|
+
first_matching_frames = _get_all_matching_frames(frames[0], all_frames)
|
|
422
|
+
if len(first_matching_frames) == 0:
|
|
423
|
+
raise ValueError(f"No frame in the traceback matched the frame: "
|
|
424
|
+
f"{':'.join(map(str, frames[0]))!a}")
|
|
425
|
+
last_matching_frames = _get_all_matching_frames(frames[1], all_frames)
|
|
426
|
+
if len(last_matching_frames) == 0:
|
|
427
|
+
raise ValueError(f"No frame in the traceback matched the frame: "
|
|
428
|
+
f"{':'.join(map(str, frames[1]))!a}")
|
|
429
|
+
# Take out the minimum and maximum indexes of the matching frames.
|
|
430
|
+
first_idxs = (first_matching_frames[0][0], first_matching_frames[-1][0])
|
|
431
|
+
last_idxs = (last_matching_frames[0][0], last_matching_frames[-1][0])
|
|
432
|
+
# Find the maximum absolute distance between the start and end matching
|
|
433
|
+
# frame indexes, and get all the frames in between that range.
|
|
434
|
+
distances = [
|
|
435
|
+
(abs(first_idxs[0] - last_idxs[0]), (first_idxs[0], last_idxs[0])),
|
|
436
|
+
(abs(first_idxs[0] - last_idxs[1]), (first_idxs[0], last_idxs[1])),
|
|
437
|
+
(abs(first_idxs[1] - last_idxs[0]), (first_idxs[1], last_idxs[0])),
|
|
438
|
+
(abs(first_idxs[1] - last_idxs[1]), (first_idxs[1], last_idxs[1])),
|
|
439
|
+
]
|
|
440
|
+
_, max_distance_pair = max(distances, key=lambda x: x[0])
|
|
441
|
+
max_distance_pair = sorted(max_distance_pair)
|
|
442
|
+
for idx in range(max_distance_pair[0], max_distance_pair[1] + 1):
|
|
443
|
+
filtered_frames.append((idx, all_frames[idx-1]))
|
|
444
|
+
|
|
445
|
+
# Only keep the unique frames and sort them using their index.
|
|
446
|
+
filtered_frames = sorted(filtered_frames, key=lambda f: f[0])
|
|
447
|
+
seen_frames = set()
|
|
448
|
+
unique_filtered_frames = []
|
|
449
|
+
# In chained exceptions, we can get the same frame at multiple indexes, so
|
|
450
|
+
# get the unique frames without considering the index.
|
|
451
|
+
for frame in filtered_frames:
|
|
452
|
+
if frame[1] not in seen_frames:
|
|
453
|
+
unique_filtered_frames.append(frame)
|
|
454
|
+
seen_frames.add(frame[1])
|
|
455
|
+
return unique_filtered_frames
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _get_all_frames_from_exception_obj(exception_obj):
|
|
459
|
+
"""
|
|
460
|
+
Get all the frame objects from the exception object. It also handles chained
|
|
461
|
+
exceptions.
|
|
462
|
+
|
|
463
|
+
:param exception_obj:
|
|
464
|
+
The exception raise by the user's code.
|
|
465
|
+
:return:
|
|
466
|
+
A list containing all the frame objects from the exception. The frames
|
|
467
|
+
are stored in bottom-to-top order, with the bottom frame (the error frame)
|
|
468
|
+
at index 0.
|
|
469
|
+
"""
|
|
470
|
+
current_exception = exception_obj
|
|
471
|
+
all_frames = []
|
|
472
|
+
while current_exception:
|
|
473
|
+
traceback = current_exception.__traceback__
|
|
474
|
+
current_tb_frames = []
|
|
475
|
+
while traceback:
|
|
476
|
+
current_tb_frames.append(traceback.tb_frame)
|
|
477
|
+
traceback = traceback.tb_next
|
|
478
|
+
all_frames.extend(reversed(current_tb_frames))
|
|
479
|
+
current_exception = (current_exception.__cause__ or
|
|
480
|
+
current_exception.__context__)
|
|
481
|
+
return all_frames
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _get_all_frames_from_current_frame(current_frame):
|
|
485
|
+
"""
|
|
486
|
+
Get all frame objects starting from the current frame up the call stack.
|
|
487
|
+
|
|
488
|
+
:param current_frame:
|
|
489
|
+
The current frame in the debugger.
|
|
490
|
+
:return
|
|
491
|
+
A list of all frame objects in bottom-to-top order, with the bottom frame
|
|
492
|
+
(current frame) at index 0.
|
|
493
|
+
"""
|
|
494
|
+
all_frames = []
|
|
495
|
+
while current_frame:
|
|
496
|
+
func_name = current_frame.f_code.co_name
|
|
497
|
+
# We've reached the python internal frames. Break the loop.
|
|
498
|
+
if func_name == '<module>':
|
|
499
|
+
break
|
|
500
|
+
all_frames.append(current_frame)
|
|
501
|
+
current_frame = current_frame.f_back
|
|
502
|
+
return all_frames
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _save_frames_and_exception_info_to_file(
|
|
506
|
+
filename, frames, variables, exclude_variables, *,
|
|
507
|
+
exception_obj=None, current_frame=None):
|
|
508
|
+
"""
|
|
509
|
+
Save the frames and exception information in the file ``filename``.
|
|
510
|
+
|
|
511
|
+
The data structure that gets saved is a dictionary. It stores each frame
|
|
512
|
+
info in a separate entry with the key as the frame index (from the bottom of
|
|
513
|
+
the stack trace). It also stores some useful exception information. The data
|
|
514
|
+
gets dumped in the ``filename`` in pickled form. Following is the same
|
|
515
|
+
structure of the info saved:
|
|
516
|
+
{
|
|
517
|
+
# 5th frame from the bottom
|
|
518
|
+
5: {
|
|
519
|
+
'frame_index': 5,
|
|
520
|
+
'filename': '/path/to/file.py',
|
|
521
|
+
'lineno': 3423,
|
|
522
|
+
'function_name': 'func1',
|
|
523
|
+
'function_qualname': 'FooClass.func1',
|
|
524
|
+
'function_object': <pickled object>,
|
|
525
|
+
'module_name': '<frame_module>'
|
|
526
|
+
'frame_identifier': '/path/to/file.py,3423,func1',
|
|
527
|
+
'code': '... python code line ...'
|
|
528
|
+
'variables': {'local_variable1': <pickled value>, 'local_variable2': <pickled value>, ...}
|
|
529
|
+
},
|
|
530
|
+
# 17th frame from the bottom
|
|
531
|
+
17: {
|
|
532
|
+
'frame_index': 17,
|
|
533
|
+
...
|
|
534
|
+
},
|
|
535
|
+
...
|
|
536
|
+
'exception_full_string': f'{exc.__class.__name__}: {exc}'
|
|
537
|
+
'exception_object': exc,
|
|
538
|
+
'exception_string': str(exc),
|
|
539
|
+
'exception_class_name': exc.__class__.__name__,
|
|
540
|
+
'exception_class_qualname': exc.__class__.__qualname__,
|
|
541
|
+
'traceback': '(multiline traceback)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
NOTE: Exception info (such as 'exception_*') will not be stored if
|
|
545
|
+
``exception_obj`` is None.
|
|
546
|
+
|
|
547
|
+
:param filename:
|
|
548
|
+
The file path in which to save the information.
|
|
549
|
+
:param frames:
|
|
550
|
+
The frames to save in the file. This parameter stores the parsed frames
|
|
551
|
+
returned by the `_validate_frames` function.
|
|
552
|
+
:param variables:
|
|
553
|
+
The local variables to include in each frame.
|
|
554
|
+
:param exclude_variables:
|
|
555
|
+
The local variables to exclude from each frame.
|
|
556
|
+
:param exception_obj:
|
|
557
|
+
The ``Exception`` raised by the user's code. This is used to extract all
|
|
558
|
+
the required info; the traceback, all the frame objects, etc.
|
|
559
|
+
:param current_frame:
|
|
560
|
+
The current frame if the user is in a debugger. This is used to extract all
|
|
561
|
+
the required info; the traceback, all the frame objects, etc.
|
|
562
|
+
"""
|
|
563
|
+
# Mapping that stores all the information to save.
|
|
564
|
+
frames_and_exception_info = {}
|
|
565
|
+
if exception_obj:
|
|
566
|
+
# Get the list of frame objects from the exception object.
|
|
567
|
+
all_frames = _get_all_frames_from_exception_obj(
|
|
568
|
+
exception_obj=exception_obj)
|
|
569
|
+
else:
|
|
570
|
+
all_frames = _get_all_frames_from_current_frame(
|
|
571
|
+
current_frame=current_frame)
|
|
572
|
+
|
|
573
|
+
# Take out the frame objects we want to save as per 'frames'.
|
|
574
|
+
frames_to_save = _get_frames_to_save(frames, all_frames)
|
|
575
|
+
_SAVEFRAME_LOGGER.info(
|
|
576
|
+
"Number of frames that'll be saved: %s", len(frames_to_save))
|
|
577
|
+
|
|
578
|
+
for frame_idx, frame_obj in frames_to_save:
|
|
579
|
+
_SAVEFRAME_LOGGER.info(
|
|
580
|
+
"Getting required info for the frame: %s", _get_frame_repr(frame_obj))
|
|
581
|
+
frames_and_exception_info[frame_idx] = _get_frame_metadata(
|
|
582
|
+
frame_idx, frame_obj).__dict__
|
|
583
|
+
frames_and_exception_info[frame_idx]['variables'] = (
|
|
584
|
+
_get_frame_local_variables_data(frame_obj, variables, exclude_variables))
|
|
585
|
+
|
|
586
|
+
if exception_obj:
|
|
587
|
+
_SAVEFRAME_LOGGER.info("Getting exception metadata info.")
|
|
588
|
+
frames_and_exception_info.update(_get_exception_info(
|
|
589
|
+
exception_obj).__dict__)
|
|
590
|
+
_SAVEFRAME_LOGGER.info("Saving the complete data in the file: %a", filename)
|
|
591
|
+
with _open_file(filename, 'wb') as f:
|
|
592
|
+
pickle.dump(frames_and_exception_info, f, protocol=PICKLE_PROTOCOL)
|
|
593
|
+
_SAVEFRAME_LOGGER.info("Done!!")
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _is_dir_and_ancestors_world_traversable(directory):
|
|
597
|
+
"""
|
|
598
|
+
Is the ``directory`` and all its ancestors world traversable.
|
|
599
|
+
|
|
600
|
+
For world traversability we check if the execute bit is set for the
|
|
601
|
+
owner, group and others.
|
|
602
|
+
|
|
603
|
+
:param directory:
|
|
604
|
+
The directory to check.
|
|
605
|
+
:return:
|
|
606
|
+
`True` if ``directory`` and its ancestors are world traversable, else
|
|
607
|
+
`False`.
|
|
608
|
+
"""
|
|
609
|
+
required_mode = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
610
|
+
dir_permission_mode = os.stat(directory).st_mode & 0o777
|
|
611
|
+
if (dir_permission_mode & required_mode) != required_mode:
|
|
612
|
+
return False
|
|
613
|
+
return directory == "/" or _is_dir_and_ancestors_world_traversable(
|
|
614
|
+
os.path.dirname(directory))
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _validate_filename(filename, utility):
|
|
618
|
+
"""
|
|
619
|
+
Validate the value of ``filename``.
|
|
620
|
+
|
|
621
|
+
:param filename:
|
|
622
|
+
The file path to validate.
|
|
623
|
+
:param utility:
|
|
624
|
+
Indicates whether this helper is invoked by the ``pyflyby.saveframe`` function
|
|
625
|
+
or the ``pyflyby/bin/saveframe`` script. See `_validate_saveframe_arguments`
|
|
626
|
+
for more info.
|
|
627
|
+
:return:
|
|
628
|
+
The file path post validation. If ``filename`` is None, a default file
|
|
629
|
+
named `DEFAULT_FILENAME` in the current working directory is returned.
|
|
630
|
+
"""
|
|
631
|
+
if filename is None:
|
|
632
|
+
filename = os.path.abspath(DEFAULT_FILENAME)
|
|
633
|
+
_SAVEFRAME_LOGGER.info(
|
|
634
|
+
"Filename is not passed explicitly using the %s. The frame info will "
|
|
635
|
+
"be saved in the file: %a.",
|
|
636
|
+
'`filename` parameter' if utility == 'function' else '--filename argument',
|
|
637
|
+
filename)
|
|
638
|
+
_SAVEFRAME_LOGGER.info("Validating filename: %a", filename)
|
|
639
|
+
# Resolve any symlinks.
|
|
640
|
+
filename = os.path.realpath(filename)
|
|
641
|
+
if os.path.islink(filename):
|
|
642
|
+
raise ValueError(f"Cyclic link exists in the file: {filename!a}")
|
|
643
|
+
if os.path.isdir(filename):
|
|
644
|
+
raise ValueError(f"{filename!a} is an already existing directory. Please "
|
|
645
|
+
f"pass a different filename.")
|
|
646
|
+
if os.path.exists(filename):
|
|
647
|
+
_SAVEFRAME_LOGGER.info(
|
|
648
|
+
"File %a already exists. This run will overwrite the file.", filename)
|
|
649
|
+
parent_dir = os.path.dirname(filename)
|
|
650
|
+
# Check if the parent directory and the ancestors are world traversable.
|
|
651
|
+
# Log a warning if not. Raise an error if the parent or any ancestor
|
|
652
|
+
# directory doesn't exist.
|
|
653
|
+
try:
|
|
654
|
+
is_parent_and_ancestors_world_traversable = (
|
|
655
|
+
_is_dir_and_ancestors_world_traversable(directory=parent_dir))
|
|
656
|
+
except PermissionError:
|
|
657
|
+
is_parent_and_ancestors_world_traversable = False
|
|
658
|
+
except (FileNotFoundError, NotADirectoryError) as err:
|
|
659
|
+
msg = (f"Error while saving the frames to the file: "
|
|
660
|
+
f"{filename!a}. Error: {err!a}")
|
|
661
|
+
raise type(err)(msg) from None
|
|
662
|
+
except OSError as err:
|
|
663
|
+
is_parent_and_ancestors_world_traversable = False
|
|
664
|
+
_SAVEFRAME_LOGGER.warning(
|
|
665
|
+
"Error while trying to determine if the parent directory: %a and "
|
|
666
|
+
"the ancestors are world traversable. Error: %a", parent_dir, err)
|
|
667
|
+
if not is_parent_and_ancestors_world_traversable:
|
|
668
|
+
_SAVEFRAME_LOGGER.warning(
|
|
669
|
+
"The parent directory %a or an ancestor is not world traversable "
|
|
670
|
+
"(i.e., the execute bit of one of the ancestors is 0). The filename "
|
|
671
|
+
"%a might not be accessible by others.", parent_dir, filename)
|
|
672
|
+
return filename
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _validate_frames(frames, utility):
|
|
676
|
+
"""
|
|
677
|
+
Validate the value of ``frames``.
|
|
678
|
+
|
|
679
|
+
This utility validates / parses the ``frames`` based on the following formats:
|
|
680
|
+
1. Single frame: frames=frame or frames=['frame']
|
|
681
|
+
2. Multiple frames: frames='frame1,frame2,...' (for `pyflyby/bin/saveframe`
|
|
682
|
+
script) or frames=['frame1', 'frame2', ...] (for `saveframe` function).
|
|
683
|
+
3. Range of frames: frames='first_frame..last_frame'
|
|
684
|
+
4. Range from first frame to bottom: frames='first_frame..'
|
|
685
|
+
5. Number of frames from bottom: frames=num
|
|
686
|
+
|
|
687
|
+
NOTE: Each frame is represented as 'file_regex:lineno:function_name'
|
|
688
|
+
|
|
689
|
+
:param frames:
|
|
690
|
+
Frames to validate
|
|
691
|
+
:param utility:
|
|
692
|
+
Indicates whether this helper is invoked by the ``pyflyby.saveframe`` function
|
|
693
|
+
or the ``pyflyby/bin/saveframe`` script. See `_validate_saveframe_arguments`
|
|
694
|
+
for more info.
|
|
695
|
+
:return:
|
|
696
|
+
A tuple of 2 items:
|
|
697
|
+
1. Parsed frames:
|
|
698
|
+
- None if ``frames`` is None.
|
|
699
|
+
- If ``frames=num`` (case 5), this is the integer ``num``.
|
|
700
|
+
- Otherwise, this is a list of tuples. Each tuple represents a frame
|
|
701
|
+
and consists of three items: ``filename``, ``lineno``, and ``function_name``.
|
|
702
|
+
- Example: For ``frames='/some/foo.py:32:,/some/bar.py:28:func'``, the
|
|
703
|
+
first item would be ``[('/some/foo.py', 32, ''), ('/some/bar.py', 28, 'func')]``.
|
|
704
|
+
|
|
705
|
+
2. The format of the ``frames``:
|
|
706
|
+
- None if ``frames`` is None.
|
|
707
|
+
- For cases 1 and 2, the format is `FrameFormat.LIST`.
|
|
708
|
+
- For cases 3 and 4, the format is `FrameFormat.RANGE`.
|
|
709
|
+
- For case 5, the format is `FrameFormat.NUM`.
|
|
710
|
+
"""
|
|
711
|
+
if frames is None:
|
|
712
|
+
_SAVEFRAME_LOGGER.info(
|
|
713
|
+
"%s is not passed explicitly. The first frame from the bottom will be "
|
|
714
|
+
"saved by default.",
|
|
715
|
+
'`frames` parameter' if utility == 'function' else '--frames argument')
|
|
716
|
+
return None, None
|
|
717
|
+
_SAVEFRAME_LOGGER.info("Validating frames: %a", frames)
|
|
718
|
+
try:
|
|
719
|
+
# Handle frames as an integer.
|
|
720
|
+
return int(frames), FrameFormat.NUM
|
|
721
|
+
except (ValueError, TypeError):
|
|
722
|
+
pass
|
|
723
|
+
# Boolean to denote if the `frames` parameter is passed in the range format.
|
|
724
|
+
is_range = False
|
|
725
|
+
if isinstance(frames, str) and ',' in frames and utility == 'function':
|
|
726
|
+
raise ValueError(
|
|
727
|
+
f"Error while validating frames: {frames!a}. If you want to pass multiple "
|
|
728
|
+
f"frames, pass a list/tuple of frames like {frames.split(',')} rather "
|
|
729
|
+
f"than a comma separated string of frames.")
|
|
730
|
+
if isinstance(frames, (list, tuple)):
|
|
731
|
+
for frame in frames:
|
|
732
|
+
if ',' in frame:
|
|
733
|
+
raise ValueError(
|
|
734
|
+
f"Invalid frame: {frame!a} in frames: {frames} as it "
|
|
735
|
+
f"contains character ','. If you are trying to pass multiple "
|
|
736
|
+
f"frames, pass them as separate items in the list.")
|
|
737
|
+
frames = ','.join(frames)
|
|
738
|
+
all_frames = [frame.strip() for frame in frames.split(',')]
|
|
739
|
+
# Handle the single frame and the range of frame formats.
|
|
740
|
+
if len(all_frames) == 1:
|
|
741
|
+
all_frames = [frame.strip() for frame in frames.split('..')]
|
|
742
|
+
if len(all_frames) > 2:
|
|
743
|
+
raise ValueError(
|
|
744
|
+
f"Error while validating frames: {frames!a}. If you want to pass a "
|
|
745
|
+
f"range of frames, the correct syntax is 'first_frame..last_frame'")
|
|
746
|
+
elif len(all_frames) == 2:
|
|
747
|
+
is_range = True
|
|
748
|
+
else:
|
|
749
|
+
is_range = False
|
|
750
|
+
|
|
751
|
+
parsed_frames = []
|
|
752
|
+
for idx, frame in enumerate(all_frames):
|
|
753
|
+
frame_parts = frame.split(':')
|
|
754
|
+
# Handle 'first_frame..' format (case 4.).
|
|
755
|
+
if idx == 1 and len(frame_parts) == 1 and frame_parts[0] == '' and is_range:
|
|
756
|
+
parsed_frames.append(frame_parts)
|
|
757
|
+
break
|
|
758
|
+
if len(frame_parts) != 3:
|
|
759
|
+
raise ValueError(
|
|
760
|
+
f"Error while validating frame: {frame!a}. The correct syntax for a "
|
|
761
|
+
f"frame is 'file_regex:line_no:function_name' but frame {frame!a} "
|
|
762
|
+
f"contains {len(frame_parts)-1} ':'.")
|
|
763
|
+
if not frame_parts[0]:
|
|
764
|
+
raise ValueError(
|
|
765
|
+
f"Error while validating frame: {frame!a}. The filename / file "
|
|
766
|
+
f"regex must be passed in a frame.")
|
|
767
|
+
# Validate the line number passed in the frame.
|
|
768
|
+
if frame_parts[1]:
|
|
769
|
+
try:
|
|
770
|
+
frame_parts[1] = int(frame_parts[1])
|
|
771
|
+
except ValueError:
|
|
772
|
+
raise ValueError(f"Error while validating frame: {frame!a}. The "
|
|
773
|
+
f"line number {frame_parts[1]!a} can't be "
|
|
774
|
+
f"converted to an integer.")
|
|
775
|
+
parsed_frames.append(frame_parts)
|
|
776
|
+
|
|
777
|
+
return parsed_frames, FrameFormat.RANGE if is_range else FrameFormat.LIST
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _is_variable_name_valid(name):
|
|
781
|
+
"""
|
|
782
|
+
Is ``name`` a valid variable name.
|
|
783
|
+
|
|
784
|
+
:param name:
|
|
785
|
+
Variable name to validate.
|
|
786
|
+
:return:
|
|
787
|
+
`True` or `False`.
|
|
788
|
+
"""
|
|
789
|
+
if not name.isidentifier():
|
|
790
|
+
return False
|
|
791
|
+
if keyword.iskeyword(name):
|
|
792
|
+
return False
|
|
793
|
+
return True
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _validate_variables(variables, utility):
|
|
797
|
+
"""
|
|
798
|
+
Validate the value of ``variables``.
|
|
799
|
+
|
|
800
|
+
If there are invalid variable names, filter them out and log a warning.
|
|
801
|
+
|
|
802
|
+
:param variables:
|
|
803
|
+
Variables to validate
|
|
804
|
+
:param utility:
|
|
805
|
+
Indicates whether this helper is invoked by the ``pyflyby.saveframe`` function
|
|
806
|
+
or the ``pyflyby/bin/saveframe`` script. See `_validate_saveframe_arguments`
|
|
807
|
+
for more info.
|
|
808
|
+
:return:
|
|
809
|
+
A tuple of filtered variables post validation.
|
|
810
|
+
"""
|
|
811
|
+
if variables is None:
|
|
812
|
+
return
|
|
813
|
+
_SAVEFRAME_LOGGER.info("Validating variables: %a", variables)
|
|
814
|
+
if isinstance(variables, str) and ',' in variables and utility == 'function':
|
|
815
|
+
raise ValueError(
|
|
816
|
+
f"Error while validating variables: {variables!a}. If you want to "
|
|
817
|
+
f"pass multiple variable names, pass a list/tuple of names like "
|
|
818
|
+
f"{variables.split(',')} rather than a comma separated string of names.")
|
|
819
|
+
if isinstance(variables, (list, tuple)):
|
|
820
|
+
all_variables = tuple(variables)
|
|
821
|
+
elif isinstance(variables, str):
|
|
822
|
+
all_variables = tuple(variable.strip() for variable in variables.split(','))
|
|
823
|
+
else:
|
|
824
|
+
raise TypeError(
|
|
825
|
+
f"Variables '{variables}' must be of type list, tuple or string (for a "
|
|
826
|
+
f"single variable), not '{type(variables)}'")
|
|
827
|
+
invalid_variable_names = [variable for variable in all_variables
|
|
828
|
+
if not _is_variable_name_valid(variable)]
|
|
829
|
+
if invalid_variable_names:
|
|
830
|
+
_SAVEFRAME_LOGGER.warning(
|
|
831
|
+
"Invalid variable names: %s. Skipping these variables and continuing.",
|
|
832
|
+
invalid_variable_names)
|
|
833
|
+
# Filter out invalid variables.
|
|
834
|
+
all_variables = tuple(variable for variable in all_variables
|
|
835
|
+
if variable not in invalid_variable_names)
|
|
836
|
+
return all_variables
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _validate_saveframe_arguments(
|
|
840
|
+
filename, frames, variables, exclude_variables, utility='function'):
|
|
841
|
+
"""
|
|
842
|
+
Validate and sanitize the parameters supported by the `saveframe` function.
|
|
843
|
+
|
|
844
|
+
:param filename:
|
|
845
|
+
File path in which to save the frame's info.
|
|
846
|
+
:param frames:
|
|
847
|
+
Specific error frames to save.
|
|
848
|
+
:param variables:
|
|
849
|
+
Local variables to include in each frame info.
|
|
850
|
+
:param exclude_variables:
|
|
851
|
+
Local variables to exclude from each frame info.
|
|
852
|
+
:param utility:
|
|
853
|
+
Indicates whether this helper is invoked by the ``pyflyby.saveframe`` function
|
|
854
|
+
or the ``pyflyby/bin/saveframe`` script. Allowed values are 'function' and
|
|
855
|
+
'script'. The saveframe function and script accept different types of
|
|
856
|
+
values for their arguments. This parameter helps distinguish between them
|
|
857
|
+
to ensure proper argument and parameter validation.
|
|
858
|
+
:return:
|
|
859
|
+
A tuple of ``filename``, ``frames``, ``variables`` and ``exclude_variables``
|
|
860
|
+
post validation.
|
|
861
|
+
"""
|
|
862
|
+
allowed_utility_values = ['function', 'script']
|
|
863
|
+
if utility not in allowed_utility_values:
|
|
864
|
+
raise ValueError(
|
|
865
|
+
f"Invalid value for parameter 'utility': {utility!a}. Allowed values "
|
|
866
|
+
f"are: {allowed_utility_values}")
|
|
867
|
+
filename = _validate_filename(filename, utility)
|
|
868
|
+
frames =_validate_frames(frames, utility)
|
|
869
|
+
if variables and exclude_variables:
|
|
870
|
+
raise ValueError(
|
|
871
|
+
f"Cannot pass both {'`variables`' if utility == 'function' else '--variables'} "
|
|
872
|
+
f"and {'`exclude_variables`' if utility == 'function' else '--exclude_variables'} "
|
|
873
|
+
f"{'parameters' if utility == 'function' else 'arguments'}.")
|
|
874
|
+
variables = _validate_variables(variables, utility)
|
|
875
|
+
exclude_variables = _validate_variables(exclude_variables, utility)
|
|
876
|
+
if not (variables or exclude_variables):
|
|
877
|
+
_SAVEFRAME_LOGGER.info(
|
|
878
|
+
"Neither %s nor %s %s is passed. All the local variables from the "
|
|
879
|
+
"frames will be saved.",
|
|
880
|
+
'`variables`' if utility == 'function' else '--variables',
|
|
881
|
+
'`exclude_variables`' if utility == 'function' else '--exclude_variables',
|
|
882
|
+
'parameter' if utility == 'function' else 'argument')
|
|
883
|
+
|
|
884
|
+
return filename, frames, variables, exclude_variables
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def saveframe(filename=None, frames=None, variables=None, exclude_variables=None):
|
|
888
|
+
"""
|
|
889
|
+
Utility to save information for debugging / reproducing an issue.
|
|
890
|
+
|
|
891
|
+
Usage:
|
|
892
|
+
--------------------------------------------------------------------------
|
|
893
|
+
If you have a piece of code that is currently failing due to an issue
|
|
894
|
+
originating from upstream code, and you cannot share your private
|
|
895
|
+
code as a reproducer, use this function to save relevant information to a file.
|
|
896
|
+
|
|
897
|
+
When to use:
|
|
898
|
+
- After an Exception:
|
|
899
|
+
- In an interactive session (IPython, Jupyter Notebook, pdb/ipdb),
|
|
900
|
+
after your code raises an error, call this function to capture and
|
|
901
|
+
save error frames specific to the upstream code.
|
|
902
|
+
- Share the generated file with the upstream team, enabling them to
|
|
903
|
+
reproduce and diagnose the issue independently.
|
|
904
|
+
- Without an Exception:
|
|
905
|
+
- Even if no error has occurred, you can deliberately enter a debugger
|
|
906
|
+
(e.g., using ``ipdb.set_trace()``) and save the frames.
|
|
907
|
+
- This can be used in case you are experiencing slowness in the upstream
|
|
908
|
+
code. Save the frames using this function to provide the upstream team
|
|
909
|
+
with relevant information for further investigation.
|
|
910
|
+
|
|
911
|
+
Information saved in the file:
|
|
912
|
+
--------------------------------------------------------------------------
|
|
913
|
+
This utility captures and saves error stack frames to a file. It includes the
|
|
914
|
+
values of local variables from each stack frame, as well as metadata about each
|
|
915
|
+
frame and the exception raised by the user's code. Following is the sample
|
|
916
|
+
structure of the info saved in the file:
|
|
917
|
+
|
|
918
|
+
::
|
|
919
|
+
|
|
920
|
+
{
|
|
921
|
+
# 5th frame from the bottom
|
|
922
|
+
5: {
|
|
923
|
+
'frame_index': 5,
|
|
924
|
+
'filename': '/path/to/file.py',
|
|
925
|
+
'lineno': 3423,
|
|
926
|
+
'function_name': 'func1',
|
|
927
|
+
'function_qualname': 'FooClass.func1',
|
|
928
|
+
'function_object': <pickled object>,
|
|
929
|
+
'module_name': '<frame_module>'
|
|
930
|
+
'frame_identifier': '/path/to/file.py,3423,func1',
|
|
931
|
+
'code': '... python code line ...'
|
|
932
|
+
'variables': {'local_variable1': <pickled value>, 'local_variable2': <pickled value>, ...}
|
|
933
|
+
},
|
|
934
|
+
# 17th frame from the bottom
|
|
935
|
+
17: {
|
|
936
|
+
'frame_index': 17,
|
|
937
|
+
...
|
|
938
|
+
},
|
|
939
|
+
...
|
|
940
|
+
'exception_full_string': f'{exc.__class.__name__}: {exc}'
|
|
941
|
+
'exception_object': exc,
|
|
942
|
+
'exception_string': str(exc),
|
|
943
|
+
'exception_class_name': exc.__class__.__name__,
|
|
944
|
+
'exception_class_qualname': exc.__class__.__qualname__,
|
|
945
|
+
'traceback': '(multiline traceback)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.. note::
|
|
949
|
+
- The above data gets saved in the file in pickled form.
|
|
950
|
+
- In the above data, the key of each frame's entry is the index of that frame
|
|
951
|
+
from the bottom of the error stack trace. So the first frame from the bottom
|
|
952
|
+
(the error frame) has index 1, and so on.
|
|
953
|
+
- 'variables' key in each frame's entry stores the local variables of that frame.
|
|
954
|
+
- The 'exception_object' key stores the actual exception object but without
|
|
955
|
+
the __traceback__ info (for security reasons).
|
|
956
|
+
- Exception info (such as 'exception_*') will not be stored in the file if
|
|
957
|
+
you enter the debugger manually (e.g., using ``ipdb.set_trace()``) and
|
|
958
|
+
call ``saveframe`` without an exception being raised.
|
|
959
|
+
|
|
960
|
+
**Example usage**:
|
|
961
|
+
|
|
962
|
+
::
|
|
963
|
+
|
|
964
|
+
# In an interactive session (ipython, jupyter notebook, etc.)
|
|
965
|
+
|
|
966
|
+
>> <Your code raised an error>
|
|
967
|
+
>> saveframe(filename=/path/to/file) # Saves the first frame from the bottom
|
|
968
|
+
>> saveframe(filename=/path/to/file, frames=frames_to_save,
|
|
969
|
+
.. variables=local_variables_to_save, exclude_variables=local_variables_to_exclude)
|
|
970
|
+
|
|
971
|
+
# In an interactive debugger (pdb / ipdb)
|
|
972
|
+
|
|
973
|
+
>> <Your code raised an error>
|
|
974
|
+
>> ipdb.pm() # start a debugger
|
|
975
|
+
>> OR
|
|
976
|
+
>> <You entered the debugger using ipdb.set_trace()>
|
|
977
|
+
>> ipdb> from pyflyby import saveframe
|
|
978
|
+
>> ipdb> saveframe(filename=/path/to/file) # Saves the frame which you are currently at
|
|
979
|
+
>> ipdb> saveframe(filename=/path/to/file, frames=frames_to_save,
|
|
980
|
+
.. variables=local_variables_to_include, exclude_variables=local_variables_to_exclude)
|
|
981
|
+
|
|
982
|
+
# Let's say your code is raising an error with the following traceback:
|
|
983
|
+
|
|
984
|
+
File "dir/__init__.py", line 6, in init_func1
|
|
985
|
+
func1()
|
|
986
|
+
File "dir/mod1.py", line 14, in func1
|
|
987
|
+
func2()
|
|
988
|
+
File "dir/mod1.py", line 9, in func2
|
|
989
|
+
obj.func2()
|
|
990
|
+
File "dir/pkg1/mod2.py", line 10, in func2
|
|
991
|
+
func3()
|
|
992
|
+
File "dir/pkg1/pkg2/mod3.py", line 6, in func3
|
|
993
|
+
raise ValueError("Error is raised")
|
|
994
|
+
ValueError: Error is raised
|
|
995
|
+
|
|
996
|
+
# To save the last frame (the error frame) in file '/path/to/file', use:
|
|
997
|
+
>> saveframe(filename='/path/to/file')
|
|
998
|
+
|
|
999
|
+
# To save a specific frame like `File "dir/mod1.py", line 9, in func2`, use:
|
|
1000
|
+
>> saveframe(filename='/path/to/file', frames='mod1.py:9:func2')
|
|
1001
|
+
|
|
1002
|
+
# To save the last 3 frames from the bottom, use:
|
|
1003
|
+
>> saveframe(frames=3)
|
|
1004
|
+
|
|
1005
|
+
# To save all the frames from 'mod1.py' and 'mod2.py' files, use:
|
|
1006
|
+
>> saveframe(filename='/path/to/file', frames=['mod1.py::', 'mod2.py::'])
|
|
1007
|
+
|
|
1008
|
+
# To save a range of frames from 'mod1.py' to 'mod3.py', use:
|
|
1009
|
+
>> saveframe(frames='mod1.py::..mod3.py::')
|
|
1010
|
+
|
|
1011
|
+
# To save a range of frames from '__init__.py' till the last frame, use:
|
|
1012
|
+
>> saveframe(frames='__init__.py::..')
|
|
1013
|
+
|
|
1014
|
+
# To only save local variables 'var1' and 'var2' from the frames, use:
|
|
1015
|
+
>> saveframe(frames=<frames_to_save>, variables=['var1', 'var2'])
|
|
1016
|
+
|
|
1017
|
+
# To exclude local variables 'var1' and 'var2' from the frames, use:
|
|
1018
|
+
>> saveframe(frames=<frames_to_save>, exclude_variables=['var1', 'var2'])
|
|
1019
|
+
|
|
1020
|
+
For non-interactive use cases (e.g., a failing script or command), checkout
|
|
1021
|
+
`pyflyby/bin/saveframe` script.
|
|
1022
|
+
|
|
1023
|
+
:param filename:
|
|
1024
|
+
File path in which to save the frame information. If this file already
|
|
1025
|
+
exists, it will be overwritten; otherwise, a new file will be created
|
|
1026
|
+
with permission mode '0o644'. If this parameter is not passed, the info
|
|
1027
|
+
gets saved in the 'saveframe.pkl' file in the current working directory.
|
|
1028
|
+
|
|
1029
|
+
:param frames:
|
|
1030
|
+
Error stack frames to save. A single frame follows the format
|
|
1031
|
+
'filename:line_no:function_name', where:
|
|
1032
|
+
- filename: The file path or a regex pattern matching the file path
|
|
1033
|
+
(displayed in the stack trace) of that error frame.
|
|
1034
|
+
- line_no (Optional): The code line number (displayed in the stack trace)
|
|
1035
|
+
of that error frame.
|
|
1036
|
+
- function_name (Optional): The function name (displayed in the stack trace)
|
|
1037
|
+
of that error frame.
|
|
1038
|
+
|
|
1039
|
+
Partial frames are also supported where line_no and/or function_name can
|
|
1040
|
+
be omitted:
|
|
1041
|
+
- filename:: -> Includes all the frames that matches the filename
|
|
1042
|
+
- filename:line_no: -> Include all the frames that matches specific line
|
|
1043
|
+
in any function in the filename
|
|
1044
|
+
- filename::function_name -> Include all the frames that matches any line
|
|
1045
|
+
in the specific function in the filename
|
|
1046
|
+
|
|
1047
|
+
Following formats are supported to pass the frames:
|
|
1048
|
+
|
|
1049
|
+
1. Single frame:
|
|
1050
|
+
frames='frame'
|
|
1051
|
+
Example: frames='/path/to/file.py:24:some_func'
|
|
1052
|
+
Includes only the specified frame.
|
|
1053
|
+
|
|
1054
|
+
2. Multiple frames:
|
|
1055
|
+
frames=['frame1', 'frame2', ...]
|
|
1056
|
+
Example: frames=['/dir/foo.py:45:', '.*/dir2/bar.py:89:caller']
|
|
1057
|
+
Includes all specified frames.
|
|
1058
|
+
|
|
1059
|
+
3. Range of frames:
|
|
1060
|
+
frames='first_frame..last_frame'
|
|
1061
|
+
Example: frames='/dir/foo.py:45:get_foo../dir3/blah.py:23:myfunc'
|
|
1062
|
+
Includes all the frames from first_frame to last_frame (both inclusive).
|
|
1063
|
+
|
|
1064
|
+
4. Range from first_frame to bottom:
|
|
1065
|
+
frames='first_frame..'
|
|
1066
|
+
Example: frames='/dir/foo.py:45:get_foo..'
|
|
1067
|
+
Includes all the frames from first_frame to the bottom of the stack trace.
|
|
1068
|
+
|
|
1069
|
+
5. Number of Frames from Bottom:
|
|
1070
|
+
frames=num
|
|
1071
|
+
Example: frames=5
|
|
1072
|
+
Includes the first 'num' frames from the bottom of the stack trace.
|
|
1073
|
+
|
|
1074
|
+
Default behavior if this parameter is not passed:
|
|
1075
|
+
- When user is in a debugger (ipdb/pdb): Save the frame the user is
|
|
1076
|
+
currently at.
|
|
1077
|
+
- When user is not in a debugger: Save the first frame from the bottom
|
|
1078
|
+
(the error frame).
|
|
1079
|
+
|
|
1080
|
+
:param variables:
|
|
1081
|
+
Local variables to include in each frame. It accepts a list/tuple of
|
|
1082
|
+
variable names or a string if there is only 1 variable.
|
|
1083
|
+
|
|
1084
|
+
If this parameter is not passed, save all the local variables of the
|
|
1085
|
+
included frames.
|
|
1086
|
+
|
|
1087
|
+
:param exclude_variables:
|
|
1088
|
+
Local variables to exclude from each frame. It accepts a list/tuple of
|
|
1089
|
+
variable names or a string if there is only 1 variable.
|
|
1090
|
+
|
|
1091
|
+
If this parameter is not passed, save all the local variables of the
|
|
1092
|
+
included frames as per the ``variables`` parameter value.
|
|
1093
|
+
|
|
1094
|
+
:return:
|
|
1095
|
+
The file path in which the frame info is saved.
|
|
1096
|
+
"""
|
|
1097
|
+
current_frame = None
|
|
1098
|
+
exception_obj = None
|
|
1099
|
+
# Boolean to denote if an exception has been raised.
|
|
1100
|
+
exception_raised = True
|
|
1101
|
+
if not ((sys.version_info < (3, 12) and hasattr(sys, 'last_value')) or
|
|
1102
|
+
(sys.version_info >= (3, 12) and hasattr(sys, 'last_exc'))):
|
|
1103
|
+
exception_raised = False
|
|
1104
|
+
|
|
1105
|
+
if exception_raised:
|
|
1106
|
+
# Get the latest exception raised.
|
|
1107
|
+
exception_obj = sys.last_value if sys.version_info < (3, 12) else sys.last_exc
|
|
1108
|
+
|
|
1109
|
+
if not (exception_raised and frames):
|
|
1110
|
+
try:
|
|
1111
|
+
# Get the instance of the interactive session the user is currently in.
|
|
1112
|
+
interactive_session_obj = sys._getframe(2).f_locals.get('self')
|
|
1113
|
+
# If the user is currently in a debugger (ipdb/pdb), save the frame the
|
|
1114
|
+
# user is currently at in the debugger.
|
|
1115
|
+
if interactive_session_obj and hasattr(interactive_session_obj, 'curframe'):
|
|
1116
|
+
current_frame = interactive_session_obj.curframe
|
|
1117
|
+
except Exception as err:
|
|
1118
|
+
_SAVEFRAME_LOGGER.warning(
|
|
1119
|
+
f"Error while extracting the interactive session object: {err}")
|
|
1120
|
+
# This logic handles two scenarios:
|
|
1121
|
+
# 1. No exception is raised and the debugger is started.
|
|
1122
|
+
# 2. An exception is raised, and the user then starts a debugger manually
|
|
1123
|
+
# (e.g., via ipdb.pm()).
|
|
1124
|
+
# In both cases, we set the frame to the current frame as the default
|
|
1125
|
+
# behavior.
|
|
1126
|
+
if frames is None and current_frame:
|
|
1127
|
+
frames = (f"{current_frame.f_code.co_filename}:{current_frame.f_lineno}:"
|
|
1128
|
+
f"{_get_qualname(current_frame)}")
|
|
1129
|
+
|
|
1130
|
+
if not (exception_obj or current_frame):
|
|
1131
|
+
raise RuntimeError(
|
|
1132
|
+
"No exception has been raised, and the session is not currently "
|
|
1133
|
+
"within a debugger. Unable to save frames.")
|
|
1134
|
+
|
|
1135
|
+
_SAVEFRAME_LOGGER.info("Validating arguments passed.")
|
|
1136
|
+
filename, frames, variables, exclude_variables = _validate_saveframe_arguments(
|
|
1137
|
+
filename, frames, variables, exclude_variables)
|
|
1138
|
+
if exception_raised:
|
|
1139
|
+
_SAVEFRAME_LOGGER.info(
|
|
1140
|
+
"Saving frames and metadata for the exception: %a", exception_obj)
|
|
1141
|
+
_save_frames_and_exception_info_to_file(
|
|
1142
|
+
filename=filename, frames=frames, variables=variables,
|
|
1143
|
+
exclude_variables=exclude_variables,
|
|
1144
|
+
exception_obj=exception_obj, current_frame=current_frame)
|
|
1145
|
+
return filename
|