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.

Files changed (55) hide show
  1. pyflyby/__init__.py +61 -0
  2. pyflyby/__main__.py +9 -0
  3. pyflyby/_autoimp.py +2229 -0
  4. pyflyby/_cmdline.py +548 -0
  5. pyflyby/_comms.py +221 -0
  6. pyflyby/_dbg.py +1367 -0
  7. pyflyby/_docxref.py +379 -0
  8. pyflyby/_dynimp.py +154 -0
  9. pyflyby/_fast_iter_modules.cpython-311-x86_64-linux-gnu.so +0 -0
  10. pyflyby/_file.py +771 -0
  11. pyflyby/_flags.py +230 -0
  12. pyflyby/_format.py +186 -0
  13. pyflyby/_idents.py +227 -0
  14. pyflyby/_import_sorting.py +165 -0
  15. pyflyby/_importclns.py +658 -0
  16. pyflyby/_importdb.py +680 -0
  17. pyflyby/_imports2s.py +643 -0
  18. pyflyby/_importstmt.py +723 -0
  19. pyflyby/_interactive.py +2113 -0
  20. pyflyby/_livepatch.py +793 -0
  21. pyflyby/_log.py +104 -0
  22. pyflyby/_modules.py +641 -0
  23. pyflyby/_parse.py +1381 -0
  24. pyflyby/_py.py +2166 -0
  25. pyflyby/_saveframe.py +1145 -0
  26. pyflyby/_saveframe_reader.py +471 -0
  27. pyflyby/_util.py +458 -0
  28. pyflyby/_version.py +7 -0
  29. pyflyby/autoimport.py +20 -0
  30. pyflyby/etc/pyflyby/canonical.py +10 -0
  31. pyflyby/etc/pyflyby/common.py +27 -0
  32. pyflyby/etc/pyflyby/forget.py +10 -0
  33. pyflyby/etc/pyflyby/mandatory.py +10 -0
  34. pyflyby/etc/pyflyby/numpy.py +156 -0
  35. pyflyby/etc/pyflyby/std.py +335 -0
  36. pyflyby/importdb.py +19 -0
  37. pyflyby/libexec/pyflyby/colordiff +34 -0
  38. pyflyby/libexec/pyflyby/diff-colorize +148 -0
  39. pyflyby/share/emacs/site-lisp/pyflyby.el +108 -0
  40. pyflyby-1.10.1.data/scripts/collect-exports +76 -0
  41. pyflyby-1.10.1.data/scripts/collect-imports +58 -0
  42. pyflyby-1.10.1.data/scripts/find-import +38 -0
  43. pyflyby-1.10.1.data/scripts/list-bad-xrefs +34 -0
  44. pyflyby-1.10.1.data/scripts/prune-broken-imports +34 -0
  45. pyflyby-1.10.1.data/scripts/pyflyby-diff +34 -0
  46. pyflyby-1.10.1.data/scripts/reformat-imports +27 -0
  47. pyflyby-1.10.1.data/scripts/replace-star-imports +37 -0
  48. pyflyby-1.10.1.data/scripts/saveframe +299 -0
  49. pyflyby-1.10.1.data/scripts/tidy-imports +163 -0
  50. pyflyby-1.10.1.data/scripts/transform-imports +47 -0
  51. pyflyby-1.10.1.dist-info/METADATA +591 -0
  52. pyflyby-1.10.1.dist-info/RECORD +55 -0
  53. pyflyby-1.10.1.dist-info/WHEEL +5 -0
  54. pyflyby-1.10.1.dist-info/entry_points.txt +4 -0
  55. 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