diff --git a/bin/saveframe b/bin/saveframe index a6c07efb..903abf26 100755 --- a/bin/saveframe +++ b/bin/saveframe @@ -55,11 +55,43 @@ NOTE: Example Usage: - $ saveframe + Let's say your script / command is raising an error with the following traceback: + + File "dir/__init__.py", line 6, in init_func1 + func1() + File "dir/mod1.py", line 14, in func1 + func2() + File "dir/mod1.py", line 9, in func2 + obj.func2() + File "dir/pkg1/mod2.py", line 10, in func2 + func3() + File "dir/pkg1/pkg2/mod3.py", line 6, in func3 + raise ValueError("Error is raised") + ValueError: Error is raised + + => To save the last frame (the error frame) in file '/path/to/file', use: $ saveframe --filename=/path/to/file - $ saveframe --frames=frames_to_save - $ saveframe --variables=local_variables_to_include - $ saveframe --exclude_variables=local_variables_to_exclude + + => To save a specific frame like `File "dir/mod1.py", line 9, in func2`, use: + $ saveframe --filename=/path/to/file --frames=mod1.py:9:func2 + + => To save the last 3 frames from the bottom, use: + $ saveframe --frames=3 + + => To save all the frames from 'mod1.py' and 'mod2.py' files, use: + $ saveframe --filename=/path/to/file --frames=mod1.py::,mod2.py:: + + => To save a range of frames from 'mod1.py' to 'mod3.py', use: + $ saveframe --frames=mod1.py::..mod3.py:: + + => To save a range of frames from '__init__.py' till the last frame, use: + $ saveframe --frames=__init__.py::.. + + => To only save local variables 'var1' and 'var2' from the frames, use: + $ saveframe --frames=frames_to_save --variables=var1,var2 + + => To exclude local variables 'var1' and 'var2' from the frames, use: + $ saveframe --frames=frames_to_save --exclude_variables=var1,var2 For interactive use cases, checkout pyflyby.saveframe function. """ @@ -248,11 +280,11 @@ def main(): # KeyboardInterrupt rather than BaseException, since we don't want to # catch SystemExit. try: - _SAVEFRAME_LOGGER.info(f"Executing the program: {command_string!a}") + _SAVEFRAME_LOGGER.info("Executing the program: %a", command_string) run_program(command) except (Exception, KeyboardInterrupt) as err: _SAVEFRAME_LOGGER.info( - f"Saving frames and metadata info for the exception: {err!a}") + "Saving frames and metadata info for the exception: %a", err) # Save the frames and metadata info to the file. _save_frames_and_exception_info_to_file( filename=filename, frames=frames, variables=variables, diff --git a/lib/python/pyflyby/_saveframe.py b/lib/python/pyflyby/_saveframe.py index 56a13f07..4c7fd77a 100644 --- a/lib/python/pyflyby/_saveframe.py +++ b/lib/python/pyflyby/_saveframe.py @@ -141,7 +141,7 @@ def _get_exception_info(exception_obj): type(exception_obj), exception_obj, exception_obj.__traceback__)) except Exception as err: _SAVEFRAME_LOGGER.warning( - f"Error while formatting the traceback. Error: {err!a}") + "Error while formatting the traceback. Error: %a", err) tb = "Traceback couldn't be formatted" exception_info = ExceptionInfo( exception_string=str(exception_obj), @@ -207,9 +207,9 @@ def _get_frame_local_variables_data(frame, variables, exclude_variables): all_local_variables[variable], protocol=PICKLE_PROTOCOL) except Exception as err: _SAVEFRAME_LOGGER.warning( - f"Cannot pickle variable: {variable!a} for frame: " - f"{_get_frame_repr(frame)}. Error: {err!a}. Skipping this " - f"variable and continuing.") + "Cannot pickle variable: %a for frame: %s. Error: %a. Skipping " + "this variable and continuing.", + variable, _get_frame_repr(frame), err) else: local_variables_to_save[variable] = pickled_value return local_variables_to_save @@ -280,13 +280,13 @@ def _get_frame_module_name(frame): frame_module = inspect.getmodule(frame) if frame_module is not None: return frame_module.__name__ - _SAVEFRAME_LOGGER.info(f"No module found for the frame: " - f"{_get_frame_repr(frame)}") + _SAVEFRAME_LOGGER.info( + "No module found for the frame: %s", _get_frame_repr(frame)) return "Module name not found" except Exception as err: _SAVEFRAME_LOGGER.warning( - f"Module name couldn't be found for the frame: " - f"{_get_frame_repr(frame)}. Error: {err!a}") + "Module name couldn't be found for the frame: %s. Error: %a", + _get_frame_repr(frame), err) return "Module name not found" @@ -329,8 +329,8 @@ def _get_frame_metadata(frame_idx, frame_obj): frame_function_object, protocol=PICKLE_PROTOCOL) except Exception as err: _SAVEFRAME_LOGGER.info( - f"Cannot pickle the function object for the frame: " - f"{_get_frame_repr(frame_obj)}. Error: {err!a}") + "Cannot pickle the function object for the frame: %s. Error: %a", + _get_frame_repr(frame_obj), err) pickled_function = "Function object not pickleable" # Object that stores all the frame's metadata. frame_metadata = FrameMetadata( @@ -407,9 +407,9 @@ def _get_frames_to_save(frames, all_frames): elif frame_type == FrameFormat.NUM: if len(all_frames) < frames: _SAVEFRAME_LOGGER.info( - f"Number of frames to dump are {frames}, but there are only " - f"{len(all_frames)} frames in the error stack. So dumping " - f"all the frames.") + "Number of frames to dump are %s, but there are only %s frames " + "in the error stack. So dumping all the frames.", + frames, len(all_frames)) frames = len(all_frames) return [(idx+1, all_frames[idx]) for idx in range(frames)] elif frame_type == FrameFormat.LIST: @@ -538,11 +538,12 @@ def _save_frames_and_exception_info_to_file( all_frames = _get_all_frames_from_exception_obj(exception_obj) # Take out the frame objects we want to save as per 'frames'. frames_to_save = _get_frames_to_save(frames, all_frames) - _SAVEFRAME_LOGGER.info(f"Number of frames that'll be saved: {len(frames_to_save)}") + _SAVEFRAME_LOGGER.info( + "Number of frames that'll be saved: %s", len(frames_to_save)) for frame_idx, frame_obj in frames_to_save: _SAVEFRAME_LOGGER.info( - f"Getting required info for the frame: {_get_frame_repr(frame_obj)}") + "Getting required info for the frame: %s", _get_frame_repr(frame_obj)) frames_and_exception_info[frame_idx] = _get_frame_metadata( frame_idx, frame_obj).__dict__ frames_and_exception_info[frame_idx]['variables'] = ( @@ -550,7 +551,7 @@ def _save_frames_and_exception_info_to_file( _SAVEFRAME_LOGGER.info("Getting exception metadata info.") frames_and_exception_info.update(_get_exception_info(exception_obj).__dict__) - _SAVEFRAME_LOGGER.info(f"Saving the complete data in the file: {filename!a}") + _SAVEFRAME_LOGGER.info("Saving the complete data in the file: %a", filename) with _open_file(filename, 'wb') as f: pickle.dump(frames_and_exception_info, f, protocol=PICKLE_PROTOCOL) _SAVEFRAME_LOGGER.info("Done!!") @@ -594,10 +595,11 @@ def _validate_filename(filename, utility): if filename is None: filename = os.path.abspath(DEFAULT_FILENAME) _SAVEFRAME_LOGGER.info( - f"Filename is not passed explicitly using the " - f"{'`filename` parameter' if utility == 'function' else '--filename argument'}. " - f"The frame info will be saved in the file: {filename!a}.") - _SAVEFRAME_LOGGER.info(f"Validating filename: {filename!a}") + "Filename is not passed explicitly using the %s. The frame info will " + "be saved in the file: %a.", + '`filename` parameter' if utility == 'function' else '--filename argument', + filename) + _SAVEFRAME_LOGGER.info("Validating filename: %a", filename) # Resolve any symlinks. filename = os.path.realpath(filename) if os.path.islink(filename): @@ -607,13 +609,11 @@ def _validate_filename(filename, utility): f"pass a different filename.") if os.path.exists(filename): _SAVEFRAME_LOGGER.info( - f"File {filename!a} already exists. This run will " - f"overwrite the file.") + "File %a already exists. This run will overwrite the file.", filename) parent_dir = os.path.dirname(filename) # Check if the parent directory and the ancestors are world traversable. # Log a warning if not. Raise an error if the parent or any ancestor # directory doesn't exist. - is_parent_and_ancestors_world_traversable = True try: is_parent_and_ancestors_world_traversable = ( _is_dir_and_ancestors_world_traversable(directory=parent_dir)) @@ -624,15 +624,15 @@ def _validate_filename(filename, utility): f"{filename!a}. Error: {err!a}") raise type(err)(msg) from None except OSError as err: + is_parent_and_ancestors_world_traversable = False _SAVEFRAME_LOGGER.warning( - f"Error while trying to determine if the parent directory: " - f"{parent_dir!a} and the ancestors are world traversable. " - f"Error: {err!a}") + "Error while trying to determine if the parent directory: %a and " + "the ancestors are world traversable. Error: %a", parent_dir, err) if not is_parent_and_ancestors_world_traversable: _SAVEFRAME_LOGGER.warning( - f"The parent directory {parent_dir!a} or an ancestor is not world " - f"traversable (i.e., the execute bit of one of the ancestors is 0). " - f"The filename {filename!a} might not be accessible by others.") + "The parent directory %a or an ancestor is not world traversable " + "(i.e., the execute bit of one of the ancestors is 0). The filename " + "%a might not be accessible by others.", parent_dir, filename) return filename @@ -674,11 +674,11 @@ def _validate_frames(frames, utility): """ if frames is None: _SAVEFRAME_LOGGER.info( - f"{'`frames` parameter' if utility == 'function' else '--frames argument'} " - f"is not passed explicitly. The first frame from the bottom will be " - f"saved by default.") + "%s is not passed explicitly. The first frame from the bottom will be " + "saved by default.", + '`frames` parameter' if utility == 'function' else '--frames argument') return None, None - _SAVEFRAME_LOGGER.info(f"Validating frames: {frames!a}") + _SAVEFRAME_LOGGER.info("Validating frames: %a", frames) try: # Handle frames as an integer. return int(frames), FrameFormat.NUM @@ -774,7 +774,7 @@ def _validate_variables(variables, utility): """ if variables is None: return - _SAVEFRAME_LOGGER.info(f"Validating variables: {variables!a}") + _SAVEFRAME_LOGGER.info("Validating variables: %a", variables) if isinstance(variables, str) and ',' in variables and utility == 'function': raise ValueError( f"Error while validating variables: {variables!a}. If you want to " @@ -792,8 +792,8 @@ def _validate_variables(variables, utility): if not _is_variable_name_valid(variable)] if invalid_variable_names: _SAVEFRAME_LOGGER.warning( - f"Invalid variable names: {invalid_variable_names}. Skipping these " - f"variables and continuing.") + "Invalid variable names: %s. Skipping these variables and continuing.", + invalid_variable_names) # Filter out invalid variables. all_variables = tuple(variable for variable in all_variables if variable not in invalid_variable_names) @@ -839,10 +839,11 @@ def _validate_saveframe_arguments( exclude_variables = _validate_variables(exclude_variables, utility) if not (variables or exclude_variables): _SAVEFRAME_LOGGER.info( - f"Neither {'`variables`' if utility == 'function' else '--variables'} " - f"nor {'`exclude_variables`' if utility == 'function' else '--exclude_variables'} " - f"{'parameter' if utility == 'function' else 'argument'} is passed. " - f"All the local variables from the frames will be saved.") + "Neither %s nor %s %s is passed. All the local variables from the " + "frames will be saved.", + '`variables`' if utility == 'function' else '--variables', + '`exclude_variables`' if utility == 'function' else '--exclude_variables', + 'parameter' if utility == 'function' else 'argument') return filename, frames, variables, exclude_variables @@ -928,6 +929,44 @@ def saveframe(filename=None, frames=None, variables=None, exclude_variables=None >> ipdb> saveframe(filename=/path/to/file, frames=frames_to_save, .. variables=local_variables_to_include, exclude_variables=local_variables_to_exclude) + # Let's say your code is raising an error with the following traceback: + + File "dir/__init__.py", line 6, in init_func1 + func1() + File "dir/mod1.py", line 14, in func1 + func2() + File "dir/mod1.py", line 9, in func2 + obj.func2() + File "dir/pkg1/mod2.py", line 10, in func2 + func3() + File "dir/pkg1/pkg2/mod3.py", line 6, in func3 + raise ValueError("Error is raised") + ValueError: Error is raised + + # To save the last frame (the error frame) in file '/path/to/file', use: + >> saveframe(filename='/path/to/file') + + # To save a specific frame like `File "dir/mod1.py", line 9, in func2`, use: + >> saveframe(filename='/path/to/file', frames='mod1.py:9:func2') + + # To save the last 3 frames from the bottom, use: + >> saveframe(frames=3) + + # To save all the frames from 'mod1.py' and 'mod2.py' files, use: + >> saveframe(filename='/path/to/file', frames=['mod1.py::', 'mod2.py::']) + + # To save a range of frames from 'mod1.py' to 'mod3.py', use: + >> saveframe(frames='mod1.py::..mod3.py::') + + # To save a range of frames from '__init__.py' till the last frame, use: + >> saveframe(frames='__init__.py::..') + + # To only save local variables 'var1' and 'var2' from the frames, use: + >> saveframe(frames=, variables=['var1', 'var2']) + + # To exclude local variables 'var1' and 'var2' from the frames, use: + >> saveframe(frames=, exclude_variables=['var1', 'var2']) + For non-interactive use cases (e.g., a failing script or command), checkout `pyflyby/bin/saveframe` script. @@ -1005,7 +1044,8 @@ def saveframe(filename=None, frames=None, variables=None, exclude_variables=None :return: The file path in which the frame info is saved. """ - if not hasattr(sys, 'last_value'): + if not ((sys.version_info < (3, 12) and hasattr(sys, 'last_value')) or + (sys.version_info >= (3, 12) and hasattr(sys, 'last_exc'))): raise RuntimeError( "No exception is raised currently for which to save the frames. " "Make sure that an uncaught exception is raised before calling the " @@ -1027,7 +1067,7 @@ def saveframe(filename=None, frames=None, variables=None, exclude_variables=None filename, frames, variables, exclude_variables = _validate_saveframe_arguments( filename, frames, variables, exclude_variables) _SAVEFRAME_LOGGER.info( - f"Saving frames and metadata for the exception: '{exception_obj!a}") + "Saving frames and metadata for the exception: %a", exception_obj) _save_frames_and_exception_info_to_file( filename=filename, frames=frames, variables=variables, exclude_variables=exclude_variables, exception_obj=exception_obj) diff --git a/setup.py b/setup.py index 9b8c179d..fa6e9959 100755 --- a/setup.py +++ b/setup.py @@ -200,6 +200,7 @@ def make_distribution(self): 'bin/pyflyby-diff', 'bin/reformat-imports', 'bin/replace-star-imports', + 'bin/saveframe', 'bin/tidy-imports', 'bin/transform-imports', ], diff --git a/tests/test_saveframe.py b/tests/test_saveframe.py index cd9ecc12..8993d6e0 100644 --- a/tests/test_saveframe.py +++ b/tests/test_saveframe.py @@ -76,6 +76,21 @@ def run_command(command): return result.stderr.decode('utf-8').strip().split('\n') +@contextmanager +def run_code_and_set_exception(code, exception): + try: + exec(code) + except exception as err: + if VERSION_INFO < (3, 12): + sys.last_value = err + else: + sys.last_exc = err + try: + yield + finally: + delattr(sys, "last_value" if VERSION_INFO < (3, 12) else "last_exc") + + def frames_metadata_checker(tmpdir, pkg_name, filename): """ Check if the metadata of the frames is correctly written in the ``filename``. @@ -325,236 +340,194 @@ def func3(): def test_saveframe_invalid_filename_1(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / pkg_name)) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / pkg_name)) err_msg = (f"{str(tmpdir / pkg_name)!a} is an already existing directory. " f"Please pass a different filename.") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_filename_2(tmpdir): pkg_name = create_pkg(tmpdir) filename = str(tmpdir / f"saveframe_dir_{get_random()}" / f"saveframe_{get_random()}.pkl") - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(FileNotFoundError) as err: - saveframe(filename=filename) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(FileNotFoundError) as err: + saveframe(filename=filename) err_msg = (f"Error while saving the frames to the file: {filename!a}. Error: " f"FileNotFoundError(2, 'No such file or directory')") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_frames_1(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames="foo.py:") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames="foo.py:") err_msg = ("Error while validating frame: 'foo.py:'. The correct syntax for " "a frame is 'file_regex:line_no:function_name' but frame 'foo.py:' " "contains 1 ':'.") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_frames_2(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=":12:func1") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames=":12:func1") err_msg = ("Error while validating frame: ':12:func1'. The filename / file " "regex must be passed in a frame.") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_frames_3(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames="file.py:12:func1,file1.py::") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames="file.py:12:func1,file1.py::") err_msg = ("Error while validating frames: 'file.py:12:func1,file1.py::'. " "If you want to pass multiple frames, pass a list/tuple of frames " "like ['file.py:12:func1', 'file1.py::'] rather than a comma " "separated string of frames.") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_frames_4(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=["file.py:12:func1", "file1.py::,file2.py:34:func2"]) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames=["file.py:12:func1", "file1.py::,file2.py:34:func2"]) err_msg = ( "Invalid frame: 'file1.py::,file2.py:34:func2' in frames: ['file.py:12:func1', " "'file1.py::,file2.py:34:func2'] as it contains character ','. If you are " "trying to pass multiple frames, pass them as separate items in the list.") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_frames_5(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames="file.py:12:func1..file2.py::..") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames="file.py:12:func1..file2.py::..") err_msg = ("Error while validating frames: 'file.py:12:func1..file2.py::..'. " "If you want to pass a range of frames, the correct syntax is " "'first_frame..last_frame'") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_frames_6(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames="file.py:foo:func1") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames="file.py:foo:func1") err_msg = ("Error while validating frame: 'file.py:foo:func1'. The line " "number 'foo' can't be converted to an integer.") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_variables_and_exclude_variables(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(variables="foo", exclude_variables="bar") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(variables="foo", exclude_variables="bar") err_msg = "Cannot pass both `variables` and `exclude_variables` parameters." assert str(err.value) == err_msg def test_saveframe_invalid_variables_1(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - variables="var1,var2") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + variables="var1,var2") err_msg = ("Error while validating variables: 'var1,var2'. If you want to pass " "multiple variable names, pass a list/tuple of names like ['var1', " "'var2'] rather than a comma separated string of names.") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_variables_2(tmpdir, caplog): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - variables=["var1", "1var2"]) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + variables=["var1", "1var2"]) log_messages = [record.message for record in caplog.records] warning_msg = ("Invalid variable names: ['1var2']. Skipping these variables " "and continuing.") assert warning_msg in log_messages - delattr(sys, "last_value") def test_saveframe_invalid_variables_3(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(TypeError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - variables=1) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(TypeError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + variables=1) err_msg = ("Variables '1' must be of type list, tuple or string (for a single " "variable), not ''") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_exclude_variables_1(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(ValueError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - exclude_variables="var1,var2") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(ValueError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + exclude_variables="var1,var2") err_msg = ("Error while validating variables: 'var1,var2'. If you want to pass " "multiple variable names, pass a list/tuple of names like ['var1', " "'var2'] rather than a comma separated string of names.") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_invalid_exclude_variables_2(tmpdir, caplog): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - exclude_variables=["var1", "1var2"]) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + exclude_variables=["var1", "1var2"]) log_messages = [record.message for record in caplog.records] warning_msg = ("Invalid variable names: ['1var2']. Skipping these variables " "and continuing.") assert warning_msg in log_messages - delattr(sys, "last_value") def test_saveframe_invalid_exclude_variables_3(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - with pytest.raises(TypeError) as err: - saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - exclude_variables=1) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with pytest.raises(TypeError) as err: + saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + exclude_variables=1) err_msg = ("Variables '1' must be of type list, tuple or string (for a single " "variable), not ''") assert str(err.value) == err_msg - delattr(sys, "last_value") def test_saveframe_no_error_raised(tmpdir): if hasattr(sys, "last_value"): delattr(sys, "last_value") + if hasattr(sys, "last_exc"): + delattr(sys, "last_exc") pkg_name = create_pkg(tmpdir) with pytest.raises(RuntimeError) as err: exec(f"from {pkg_name} import init_func2; init_func2()") @@ -567,118 +540,101 @@ def test_saveframe_no_error_raised(tmpdir): def test_saveframe_frame_format_1(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = str(tmpdir / f"saveframe_{get_random()}.pkl") - # Format: 'filename:line_no:func_name' - saveframe(filename=filename, frames="pkg1/mod2.py:10:func2") - data = load_pkl(filename) - assert set(data.keys()) == exception_info_keys | {2} - assert data[2]["filename"] == str(tmpdir / pkg_name / "pkg1" / "mod2.py") - assert data[2]["function_name"] == "func2" - - # Format: 'filename::' - saveframe(filename=filename, frames="mod1.py::") - data = load_pkl(filename) - assert set(data.keys()) == exception_info_keys | {3, 4} - assert data[3]["filename"] == str(tmpdir / pkg_name / "mod1.py") - assert data[3]["function_name"] == "func2" - - # Format: 'filename::func_name' - saveframe(filename=filename, frames=f"{pkg_name}/mod1.py::func1") - data = load_pkl(filename) - assert set(data.keys()) == exception_info_keys | {4} - assert data[4]["filename"] == str(tmpdir / pkg_name / "mod1.py") - assert data[4]["function_name"] == "func1" - delattr(sys, "last_value") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = str(tmpdir / f"saveframe_{get_random()}.pkl") + # Format: 'filename:line_no:func_name' + saveframe(filename=filename, frames="pkg1/mod2.py:10:func2") + data = load_pkl(filename) + assert set(data.keys()) == exception_info_keys | {2} + assert data[2]["filename"] == str(tmpdir / pkg_name / "pkg1" / "mod2.py") + assert data[2]["function_name"] == "func2" + + # Format: 'filename::' + saveframe(filename=filename, frames="mod1.py::") + data = load_pkl(filename) + assert set(data.keys()) == exception_info_keys | {3, 4} + assert data[3]["filename"] == str(tmpdir / pkg_name / "mod1.py") + assert data[3]["function_name"] == "func2" + + # Format: 'filename::func_name' + saveframe(filename=filename, frames=f"{pkg_name}/mod1.py::func1") + data = load_pkl(filename) + assert set(data.keys()) == exception_info_keys | {4} + assert data[4]["filename"] == str(tmpdir / pkg_name / "mod1.py") + assert data[4]["function_name"] == "func1" def test_saveframe_frame_format_2(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = str(tmpdir / f"saveframe_{get_random()}.pkl") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = str(tmpdir / f"saveframe_{get_random()}.pkl") - saveframe(filename=filename, frames=["__init__.py::", "pkg1/mod2.py:10:func2"]) - data = load_pkl(filename) - assert set(data.keys()) == exception_info_keys | {2, 5} - assert data[2]["filename"] == str(tmpdir / pkg_name / "pkg1" / "mod2.py") - assert ( - data[2]["function_qualname"] == - "func2" if VERSION_INFO < (3, 11) else "mod2_cls.func2") + saveframe(filename=filename, frames=["__init__.py::", "pkg1/mod2.py:10:func2"]) + data = load_pkl(filename) + assert set(data.keys()) == exception_info_keys | {2, 5} + assert data[2]["filename"] == str(tmpdir / pkg_name / "pkg1" / "mod2.py") + assert ( + data[2]["function_qualname"] == + "func2" if VERSION_INFO < (3, 11) else "mod2_cls.func2") - assert data[5]["filename"] == str(tmpdir / pkg_name / "__init__.py") - assert data[5]["function_qualname"] == "init_func1" + assert data[5]["filename"] == str(tmpdir / pkg_name / "__init__.py") + assert data[5]["function_qualname"] == "init_func1" - saveframe(filename=filename, frames=["pkg1/pkg2/mod3.py:6:", "mod1::"]) - data = load_pkl(filename) - assert set(data.keys()) == exception_info_keys | {1, 3, 4} - assert data[1]["filename"] == str(tmpdir / pkg_name / "pkg1" / "pkg2" / "mod3.py") - assert data[1]["function_qualname"] == "func3" - delattr(sys, "last_value") + saveframe(filename=filename, frames=["pkg1/pkg2/mod3.py:6:", "mod1::"]) + data = load_pkl(filename) + assert set(data.keys()) == exception_info_keys | {1, 3, 4} + assert data[1]["filename"] == str(tmpdir / pkg_name / "pkg1" / "pkg2" / "mod3.py") + assert data[1]["function_qualname"] == "func3" def test_saveframe_frame_format_3(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = str(tmpdir / f"saveframe_{get_random()}.pkl") - saveframe(filename=filename, frames=3) - data = load_pkl(filename) - assert set(data.keys()) == exception_info_keys | {1, 2, 3} + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = str(tmpdir / f"saveframe_{get_random()}.pkl") + saveframe(filename=filename, frames=3) + data = load_pkl(filename) + assert set(data.keys()) == exception_info_keys | {1, 2, 3} - saveframe(filename=filename, frames=5) - data = load_pkl(filename) - assert set(data.keys()) == exception_info_keys | {1, 2, 3, 4, 5} - delattr(sys, "last_value") + saveframe(filename=filename, frames=5) + data = load_pkl(filename) + assert set(data.keys()) == exception_info_keys | {1, 2, 3, 4, 5} def test_saveframe_frame_format_4(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = str(tmpdir / f"saveframe_{get_random()}.pkl") - saveframe(filename=filename, frames="pkg1/mod2.py::..__init__.py:6:init_func1") - data = load_pkl(filename) - assert set(data.keys()) == exception_info_keys | {2, 3, 4, 5} + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = str(tmpdir / f"saveframe_{get_random()}.pkl") + saveframe(filename=filename, frames="pkg1/mod2.py::..__init__.py:6:init_func1") + data = load_pkl(filename) + assert set(data.keys()) == exception_info_keys | {2, 3, 4, 5} - with pytest.raises(ValueError) as err: - saveframe(filename=filename, - frames="pkg1/mod3.py::..__init__.py:6:init_func1") - err_msg = "No frame in the traceback matched the frame: 'pkg1/mod3.py::'" - assert str(err.value) == err_msg - delattr(sys, "last_value") + with pytest.raises(ValueError) as err: + saveframe(filename=filename, + frames="pkg1/mod3.py::..__init__.py:6:init_func1") + err_msg = "No frame in the traceback matched the frame: 'pkg1/mod3.py::'" + assert str(err.value) == err_msg def test_saveframe_frame_format_5(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=f"{str(tmpdir)}/{pkg_name}/mod1.py::..") + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames=f"{str(tmpdir)}/{pkg_name}/mod1.py::..") data = load_pkl(filename) assert set(data.keys()) == exception_info_keys | {1, 2, 3, 4} - delattr(sys, "last_value") def test_saveframe_variables(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=5, variables=['var1', 'var2']) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames=5, variables=['var1', 'var2']) data = load_pkl(filename) assert set(data.keys()) == exception_info_keys | {1, 2, 3, 4, 5} @@ -687,17 +643,14 @@ def test_saveframe_variables(tmpdir): assert set(data[3]["variables"].keys()) == {"var1", "var2"} assert set(data[4]["variables"].keys()) == {"var1"} assert set(data[5]["variables"].keys()) == {"var1", "var2"} - delattr(sys, "last_value") def test_saveframe_exclude_variables(tmpdir, caplog): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=5, exclude_variables=['var1', 'var2']) + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = saveframe(filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames=5, exclude_variables=['var1', 'var2']) log_messages = [record.message for record in caplog.records] data = load_pkl(filename) assert set(data.keys()) == exception_info_keys | {1, 2, 3, 4, 5} @@ -712,18 +665,15 @@ def test_saveframe_exclude_variables(tmpdir, caplog): f"Cannot pickle variable: 'var3' for frame: 'File: {str(tmpdir)}/{pkg_name}" f"/pkg1/mod2.py, Line: 10, Function: {qualname}'.") assert warning_msg in "\n".join(log_messages) - delattr(sys, "last_value") def test_saveframe_defaults(tmpdir, caplog): pkg_name = create_pkg(tmpdir) - with chdir(tmpdir): - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = saveframe() - log_messages = [record.message for record in caplog.records] + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + with chdir(tmpdir): + filename = saveframe() + log_messages = [record.message for record in caplog.records] # Test that saveframe.pkl file in the current working directory is used by # default. info_message = (f"Filename is not passed explicitly using the `filename` " @@ -738,57 +688,42 @@ def test_saveframe_defaults(tmpdir, caplog): # Test that only first frame from the bottom (index = 1) is stored in the # data by default. assert set(data.keys()) == exception_info_keys | {1} - delattr(sys, "last_value") def test_saveframe_frame_metadata(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = saveframe( - filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=5) - + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = saveframe( + filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=5) frames_metadata_checker(tmpdir, pkg_name, filename) - delattr(sys, "last_value") def test_saveframe_local_variables_data(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = saveframe( - filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=5) - + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = saveframe( + filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=5) frames_local_variables_checker(pkg_name, filename) - delattr(sys, "last_value") def test_saveframe_exception_info(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func1; init_func1()") - except ValueError as err: - sys.last_value = err - filename = saveframe( - filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=0) - + code = f"from {pkg_name} import init_func1; init_func1()" + with run_code_and_set_exception(code, ValueError): + filename = saveframe( + filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=0) exception_info_checker(filename) - delattr(sys, "last_value") def test_saveframe_chained_exceptions(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func3; init_func3()") - except TypeError as err: - sys.last_value = err - filename = saveframe( - filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=['__init__.py::init_func3', f'.*/{pkg_name}/.*::']) + code = f"from {pkg_name} import init_func3; init_func3()" + with run_code_and_set_exception(code, TypeError): + filename = saveframe( + filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), + frames=['__init__.py::init_func3', f'.*/{pkg_name}/.*::']) data = load_pkl(filename) assert 1 in set(data.keys()) @@ -803,46 +738,33 @@ def test_saveframe_chained_exceptions(tmpdir): assert data[1]["frame_identifier"] == ( f"{data[1]['filename']},{data[1]['lineno']},{data[1]['function_name']}") assert len(set(data.keys()) - exception_info_keys) == 5 - delattr(sys, "last_value") def test_keyboard_interrupt_frame_metadata(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func4; init_func4()") - except KeyboardInterrupt as err: - sys.last_value = err - filename = saveframe( - filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=2) + code = f"from {pkg_name} import init_func4; init_func4()" + with run_code_and_set_exception(code, KeyboardInterrupt): + filename = saveframe( + filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=2) frames_metadata_checker_for_keyboard_interrupt(tmpdir, pkg_name, filename) - delattr(sys, "last_value") def test_keyboard_interrupt_local_variables_data(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func4; init_func4()") - except KeyboardInterrupt as err: - sys.last_value = err - filename = saveframe( - filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=2) + code = f"from {pkg_name} import init_func4; init_func4()" + with run_code_and_set_exception(code, KeyboardInterrupt): + filename = saveframe( + filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=2) frames_local_variables_checker_for_keyboard_interrupt(filename) - delattr(sys, "last_value") def test_keyboard_interrupt_exception_info(tmpdir): pkg_name = create_pkg(tmpdir) - try: - exec(f"from {pkg_name} import init_func4; init_func4()") - except KeyboardInterrupt as err: - sys.last_value = err - filename = saveframe( - filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), - frames=0) + code = f"from {pkg_name} import init_func4; init_func4()" + with run_code_and_set_exception(code, KeyboardInterrupt): + filename = saveframe( + filename=str(tmpdir / f"saveframe_{get_random()}.pkl"), frames=0) exception_info_checker_for_keyboard_interrupt(filename) - delattr(sys, "last_value") def test_saveframe_cmdline_no_exception():