diff --git a/etc/pyflyby/common.py b/etc/pyflyby/common.py index b9368806..996b056c 100644 --- a/etc/pyflyby/common.py +++ b/etc/pyflyby/common.py @@ -11,7 +11,7 @@ import pexpect import pstats import pyflyby -from pyflyby import saveframe, xreload +from pyflyby import SaveframeReader, saveframe, xreload import pylab import pyodbc import pysvn diff --git a/lib/python/pyflyby/__init__.py b/lib/python/pyflyby/__init__.py index 8dff1a6a..92fcc162 100644 --- a/lib/python/pyflyby/__init__.py +++ b/lib/python/pyflyby/__init__.py @@ -33,6 +33,8 @@ from pyflyby._log import logger from pyflyby._parse import PythonBlock, PythonStatement from pyflyby._saveframe import saveframe +from pyflyby._saveframe_reader \ + import SaveframeReader from pyflyby._version import __version__ # Deprecated: diff --git a/lib/python/pyflyby/_saveframe_reader.py b/lib/python/pyflyby/_saveframe_reader.py new file mode 100644 index 00000000..92422b34 --- /dev/null +++ b/lib/python/pyflyby/_saveframe_reader.py @@ -0,0 +1,444 @@ +""" +pyflyby/_saveframe_reader.py + +This module provides the ``SaveframeReader`` class, which is used to read data +saved by the ``saveframe`` utility. +""" + +from __future__ import annotations + +import logging +import pickle + +from pyflyby._saveframe import ExceptionInfo, FrameMetadata + +class SaveframeReader: + """ + A class for reading data saved by the ``saveframe`` utility. + + The ``saveframe`` utility saves data as a pickled Python dictionary. + Reading this raw data and extracting values of specific variables or metadata + fields can be complex. + + The ``SaveframeReader`` class provides an easy and efficient way to read this + raw data and extract specific items. This class has a user-friendly ``repr`` + for visualizing the data and provides various helpful methods to extract + different items. + + **Usage Example:** + + **Creating an instance** + + First, create an instance of this class by passing the path of the file that + contains the ``saveframe`` data. + + :: + + >> from pyflyby import SaveframeReader + >> reader = SaveframeReader('/path/to/file') + + **Extracting all available metadata fields** + + To extract all available metadata fields, use the ``SaveframeReader.metadata`` + property. Example: + + :: + + >> reader.metadata + ['frame_index', 'filename', 'lineno', 'function_name', 'function_qualname', + 'function_object', 'module_name', 'code', 'frame_identifier', + 'exception_string', 'exception_full_string', 'exception_class_name', + 'exception_class_qualname', 'exception_object', 'traceback'] + + **Extracting all stored local variables** + + To extract the names of all local variables stored in the frames, use the + ``SaveframeReader.variables`` property. Example: + + :: + + >> reader.variables + { + 1: ['var1', 'var2', ...], + 2: ['var5', 'var8', 'var9', ...], + ... + } + + **Extracting the value of a specific metadata field** + + To extract the value of a specific metadata field, use the + `SaveframeReader.get_metadata` method. Example: + + :: + + >> reader.get_metadata("filename") + {1: '/dir1/mod1.py', 2: '/dir2/mod2.py', ...} + + >> reader.get_metadata("filename", frame_idx=2) + '/dir2/mod2.py' + + >> reader.get_metadata("exception_string") + "Error is raised" + + **Extracting the value of specific local variables** + + To extract the value of specific local variable(s), use the + `SaveframeReader.get_variables` method. Example: + + :: + + >> reader.get_variables('var1') + {2: var1_value2, 4: var1_value4} + + >> reader.get_variables('var1', frame_idx=4) + var1_value4 + + >> reader.get_variables('var2') + var2_value3 + + >> reader.get_variables(['var1', 'var3']) + {2: {'var1': var1_value2, 'var3': var3_value2}, + 4: {'var1': var1_value4}, 5: {'var3': var3_value5}} + + >> reader.get_variables(['var1', 'var3'], frame_idx=2) + {'var1': var1_value2, 'var3': var3_value2} + + Raw data can be extracted using ``SaveframeReader.data`` property. + """ + + def __init__(self, filename): + """ + Initializes the ``SaveframeReader`` class. + + :param filename: + The file path where the ``saveframe`` data is stored. + """ + self._filename = filename + with open(filename, 'rb') as f: + self._data = pickle.load(f) + if not isinstance(self._data, dict): + raise ValueError( + f"The data in the file '{filename}' is of type " + f"'{type(self._data).__name__}', which is not valid saveframe " + "data.") + + + @property + def filename(self): + """ + The file path where the ``saveframe`` data is stored. + """ + return self._filename + + + @property + def data(self): + """ + Returns the raw ``saveframe`` data as a Python dictionary. + """ + return self._data + + + @property + def metadata(self): + """ + Returns a list of all metadata items present in the data. + + This includes both frame metadata and exception metadata. The returned + list contains the names of all metadata fields. For example: + ['frame_index', 'filename', ..., 'exception_object', 'traceback']. + + To obtain the value of a specific metadata field, use the + `SaveframeReader.get_metadata` method. + """ + metadata = [] + metadata.extend([field for field in FrameMetadata.__dataclass_fields__]) + metadata.extend([field for field in ExceptionInfo.__dataclass_fields__]) + return metadata + + + @property + def variables(self): + """ + Returns the local variables present in each frame. + + The returned value is a dictionary where the keys are frame indices and + the values are lists of local variable names in those frames. For example: + + :: + + { + 1: ['variable1', 'variable2', ...], + 2: ['variable5', 'variable6', 'variable8'], + ... + } + + To obtain the value of specific variable(s), use the + `SaveframeReader.get_variables` method. + """ + frame_idx_to_variables_map = {} + for key_item in self._data: + if not isinstance(key_item, int): + continue + frame_idx_to_variables_map[key_item] = list( + self._data[key_item]['variables'].keys()) + return frame_idx_to_variables_map + + + def get_metadata(self, metadata, frame_idx=None): + """ + Retrieve the value of a specific metadata field. + + **Example usage:** + + :: + + >> reader = SaveframeReader("/path/to/file") + + >> reader.get_metadata("filename") + {1: '/dir1/mod1.py', 2: '/dir2/mod2.py', ...} + + >> reader.get_metadata("filename", frame_idx=2) + '/dir2/mod2.py' + + >> reader.get_metadata("exception_string") + "Error is raised" + + :param metadata: + The metadata field for which to get the value. + :param frame_idx: + The index of the frame from which to get the metadata value. Default is + None, which means metadata from all frames is returned. This parameter + is only supported for frame metadata, not exception metadata. + :return: + - If ``frame_idx`` is None (default): + - If ``metadata`` is a frame metadata field, a dictionary is returned + with the frame index as the key and the metadata value as the value. + - If ``metadata`` is an exception metadata field, the value of the + metadata is returned. + - If ``frame_idx`` is specified: + - If ``metadata`` is a frame metadata field, the metadata value for + the specified frame is returned. + - If ``metadata`` is an exception metadata field, an error is raised. + """ + # Sanity checks. + all_metadata_entries = self.metadata + if metadata not in all_metadata_entries: + raise ValueError( + f"Invalid metadata requested: {metadata!a}. Allowed metadata " + f"entries are: {all_metadata_entries}.") + exception_metadata = ([field for field in ExceptionInfo.__dataclass_fields__]) + # Handle exception metadata. + if metadata in exception_metadata: + if frame_idx: + raise ValueError( + "'frame_idx' is not supported for querying exception " + f"metadata: {metadata!a}.") + return self._data[metadata] + # frame_idx is not passed. + if frame_idx is None: + frame_idx_to_metadata_value_map = {} + for key_item in self._data: + if key_item in exception_metadata: + continue + metadata_value = self._data[key_item][metadata] + # Unpickle the 'function_object' metadata value. + if metadata == "function_object": + try: + metadata_value = pickle.loads(metadata_value) + except Exception: + logging.warning("Can't unpickle the 'function_object' " + "value for frame: %a", frame_idx) + metadata_value = "Can't unpickle the 'function_object'" + frame_idx_to_metadata_value_map[key_item] = metadata_value + return frame_idx_to_metadata_value_map + + # frame_idx is passed. + if not isinstance(frame_idx, int): + raise TypeError( + "'frame_idx' must be of type 'int', not " + f"'{type(frame_idx).__name__}'.") + try: + metadata_value = self._data[frame_idx][metadata] + if metadata == "function_object": + try: + metadata_value = pickle.loads(metadata_value) + except Exception: + logging.warning("Can't unpickle the 'function_object' " + "value for frame: %a", frame_idx) + return metadata_value + except KeyError: + allowed_frame_idx = list( + set(self._data.keys()) - set(exception_metadata)) + raise ValueError( + f"Invalid value for 'frame_idx': '{frame_idx}'. Allowed values " + f"are: {allowed_frame_idx}.") + + + def get_variables(self, variables, frame_idx=None): + """ + Retrieve the value of local variable(s) from specific frames. + + **Example usage:** + + :: + + >> reader = SaveframeReader('/path/to/file') + + >> reader.get_variables('var1') + {2: var1_value2, 4: var1_value4} + + >> reader.get_variables('var1', frame_idx=4) + var1_value4 + + >> reader.get_variables('var2') + var2_value3 # 'var2' is only present in frame 3 + + >> reader.get_variables(['var1', 'var3']) + {2: {'var1': var1_value2, 'var3': var3_value2}, + 4: {'var1': var1_value4}, 5: {'var3': var3_value5}} + + >> reader.get_variables(['var1', 'var3'], frame_idx=2) + {'var1': var1_value2, 'var3': var3_value2} + + :param variables: + A string or a list of variable names for which to retrieve the values. + :param frame_idx: + The index of the frame from which to retrieve the value(s) of the + variable(s). Default is None, which means values from all frames are + returned. + :return: + - If ``frame_idx`` is None (default): + - For single variable: + - A dictionary with frame indices as keys and variable values + as values. + - If the variable is present in only one frame, the value is + returned directly. + - For multiple variables: + - A dictionary with frame indices as keys and dictionaries as + values, where each inner dictionary contains the queried + variables and their values for that frame. + - If the queried variables are present in only one frame, a + dictionary of those variables and their values is returned. + - If ``frame_idx`` is specified: + - For a single variable: + - The value of the variable in the specified frame. + - If the variable is not present in that frame, an error is raised. + - For multiple variables: + - A dictionary with the variable names as keys and their values + as values, for the specified frame. + - If none of the queried variables are present in that frame, + an error is raised. + """ + # Sanity checks. + if not isinstance(variables, (str, list, tuple)): + raise TypeError( + f"'variables' must either be a string or a list/tuple. " + f"Got '{type(variables).__name__}'.") + if isinstance(variables, (list, tuple)) and len(variables) == 0: + raise ValueError("No 'variables' passed.") + if isinstance(variables, str): + variables = [variables] + # frame_idx is not passed. + if frame_idx is None: + frame_idx_to_variables_map = {} + for key_item in self._data: + if not isinstance(key_item, int): + continue + variables_map = self._data[key_item]['variables'] + for variable in variables: + try: + variable_value = variables_map[variable] + except KeyError: + continue + try: + variable_value = pickle.loads(variable_value) + except Exception: + logging.warning( + "Can't un-pickle the value of variable %a for frame " + "%a", variable, frame_idx) + variable_value = "Can't un-pickle the variable." + if len(variables) == 1: + # Single variable is queried. + frame_idx_to_variables_map[key_item] = variable_value + else: + # Multiple variables are queried. The result would be + # a dict where keys would be the frame indices and values + # would the dicts containing the queried variables and + # their values for that frame. + if not key_item in frame_idx_to_variables_map: + frame_idx_to_variables_map[key_item] = {} + frame_idx_to_variables_map[key_item][variable] = variable_value + if not frame_idx_to_variables_map: + raise ValueError(f"Local variable(s) {variables} not found in " + "any of the saved frames.") + # If there is only 1 frame in the result, return the value directly. + if len(frame_idx_to_variables_map) == 1: + return frame_idx_to_variables_map.popitem()[1] + return frame_idx_to_variables_map + + # frame_idx is passed. + if not isinstance(frame_idx, int): + raise TypeError( + "'frame_idx' must be of type 'int', not " + f"'{type(frame_idx).__name__}'.") + try: + variables_map = self._data[frame_idx]['variables'] + except KeyError: + allowed_frame_idx = list( + set(self._data.keys()) - set(self.metadata)) + raise ValueError( + f"Invalid value for 'frame_idx': '{frame_idx}'. Allowed values " + f"are: {allowed_frame_idx}.") + variable_key_to_value_map = {} + for variable in variables: + try: + variable_value = variables_map[variable] + except KeyError: + continue + try: + variable_value = pickle.loads(variable_value) + except Exception: + logging.warning( + "Can't un-pickle the value of variable %a for frame " + "%a", variable, frame_idx) + if len(variables) > 1: + variable_value = "Can't un-pickle the variable." + if len(variables) == 1: + # Single variable is queried. Directly return the value. + return variable_value + variable_key_to_value_map[variable] = variable_value + if not variable_key_to_value_map: + raise ValueError(f"Local variable(s) {variables} not found in the " + f"frame {frame_idx}") + return variable_key_to_value_map + + + def __str__(self): + frames_info = [] + for frame_idx, frame_data in self._data.items(): + if isinstance(frame_idx, int): + frame_info = ( + f"Frame {frame_idx}:\n" + f" Filename: '{frame_data.get('filename')}'\n" + f" Line Number: {frame_data.get('lineno')}\n" + f" Function: {frame_data.get('function_qualname')}\n" + f" Module: {frame_data.get('module_name')}\n" + f" Frame ID: '{frame_data.get('frame_identifier')}'\n" + f" Code: {frame_data.get('code')}\n" + f" Variables: {list(frame_data.get('variables', {}).keys())}\n" + ) + frames_info.append(frame_info) + + exception_info = ( + f"Exception:\n" + f" Full String: {self._data.get('exception_full_string')}\n" + f" String: {self._data.get('exception_string')}\n" + f" Class Name: {self._data.get('exception_class_name')}\n" + f" Qualified Name: {self._data.get('exception_class_qualname')}\n" + ) + + return "Frames:\n" + "\n".join(frames_info) + "\n" + exception_info + + def __repr__(self): + return f"{self.__class__.__name__}(\nfilename: {self._filename!a} \n\n{str(self)})" \ No newline at end of file diff --git a/tests/test_saveframe_reader.py b/tests/test_saveframe_reader.py new file mode 100644 index 00000000..78560d66 --- /dev/null +++ b/tests/test_saveframe_reader.py @@ -0,0 +1,726 @@ +from __future__ import annotations + +from contextlib import contextmanager +import os +import pickle +import pytest +import random +from shutil import rmtree +import subprocess +import sys +from tempfile import mkdtemp +from textwrap import dedent + +from pyflyby import Filename, SaveframeReader, saveframe + +VERSION_INFO = sys.version_info + +@pytest.fixture +def tmpdir(request): + """ + A temporary directory which is temporarily added to sys.path. + """ + d = mkdtemp(prefix="pyflyby_test_saveframe_", suffix=".tmp") + d = Filename(d).real + def cleanup(): + # Unload temp modules. + for name, module in sorted(sys.modules.items()): + if (getattr(module, "__file__", None) or "").startswith(str(d)): + del sys.modules[name] + # Clean up sys.path. + sys.path.remove(str(d)) + # Clean up directory on disk. + rmtree(str(d)) + request.addfinalizer(cleanup) + sys.path.append(str(d)) + return d + + +def load_pkl(filename): + with open(filename, mode='rb') as f: + data = pickle.load(f) + return data + + +def writetext(filename, text, mode='w'): + text = dedent(text) + assert isinstance(filename, Filename) + with open(str(filename), mode) as f: + f.write(text) + return filename + + +@contextmanager +def chdir(path): + old_cwd = os.getcwd() + os.chdir(str(path)) + try: + yield + finally: + os.chdir(old_cwd) + + +def get_random(): + return int(random.random() * 10 ** 9) + + +def run_command(command): + result = subprocess.run(command, capture_output=True) + 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 create_pkg(tmpdir): + """ + Create a pacakge with multiple nested sub-packages and modules in ``tmpdir``. + """ + pkg_name = f"saveframe_{int(random.random() * 10**9)}" + os.mkdir(str(tmpdir / pkg_name)) + writetext(tmpdir / pkg_name / "__init__.py", f""" + from {pkg_name}.mod1 import func1 + def init_func1(): + var1 = 3 + var2 = 'blah' + func1() + """) + writetext(tmpdir / pkg_name / "mod1.py", f""" + import os + import signal + from {pkg_name}.pkg1.mod2 import mod2_cls + def func2(): + var1 = "func2" + var2 = 34 + obj = mod2_cls() + obj.func2() + + def func1(): + var1 = [4, 5, 2] + func1_var2 = 4.56 + func2() + """) + os.mkdir(str(tmpdir / pkg_name / "pkg1")) + writetext(tmpdir / pkg_name / "pkg1" / "__init__.py", "") + writetext(tmpdir / pkg_name / "pkg1" / "mod2.py", f""" + from {pkg_name}.pkg1.pkg2.mod3 import func3 + class mod2_cls: + def __init__(self): + pass + def func2(self): + var1 = 'foo' + var2 = (4, 9, 10) + var3 = lambda x: x+1 + func3() + """) + os.mkdir(str(tmpdir/ pkg_name / "pkg1" / "pkg2")) + writetext(tmpdir / pkg_name / "pkg1" / "pkg2" / "__init__.py", "") + writetext(tmpdir / pkg_name / "pkg1" / "pkg2" / "mod3.py", """ + def func3(): + var1 = [4, 'foo', 2.4] + var2 = 'blah' + func3_var3 = True + raise ValueError("Error is raised") + """) + return pkg_name + + +def call_saveframe(pkg_name, tmpdir, frames): + code = f"from {pkg_name} import init_func1; init_func1()" + filename = str(tmpdir / f"saveframe_{get_random()}.pkl") + with run_code_and_set_exception(code, ValueError): + saveframe(filename=filename, frames=frames) + return filename + + +def test_saveframe_reader_repr_1(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=1) + reader = SaveframeReader(filename) + + expected = ( + f'SaveframeReader(\nfilename: \'{filename}\' \n\nFrames:\nFrame 1:\n ' + f'Filename: \'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py\'\n Line Number: ' + f'6\n Function: func3\n Module: {pkg_name}.pkg1.pkg2.mod3\n Frame ID: ' + f'\'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py,6,func3\'\n Code: raise ' + 'ValueError("Error is raised")\n Variables: [\'var1\', \'var2\', \'' + 'func3_var3\']\n\nException:\n Full String: ValueError: Error is ' + 'raised\n String: Error is raised\n Class Name: ValueError\n ' + 'Qualified Name: ValueError\n)') + assert repr(reader) == expected + + +def test_saveframe_reader_repr_2(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=2) + reader = SaveframeReader(filename) + + expected = ( + f'SaveframeReader(\nfilename: \'{filename}\' \n\nFrames:\nFrame 1:\n ' + f'Filename: \'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py\'\n Line Number: ' + f'6\n Function: func3\n Module: {pkg_name}.pkg1.pkg2.mod3\n Frame ID: ' + f'\'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py,6,func3\'\n Code: raise ' + 'ValueError("Error is raised")\n Variables: [\'var1\', \'var2\', \'' + f'func3_var3\']\n\nFrame 2:\n Filename: \'{tmpdir}/{pkg_name}/pkg1/' + 'mod2.py\'\n Line Number: 10\n Function: mod2_cls.func2\n Module: ' + f'{pkg_name}.pkg1.mod2\n Frame ID: \'{tmpdir}/{pkg_name}/pkg1/mod2.py,' + '10,func2\'\n Code: func3()\n Variables: [\'self\', \'var1\', \'var2' + '\']\n\nException:\n Full String: ValueError: Error is ' + 'raised\n String: Error is raised\n Class Name: ValueError\n ' + 'Qualified Name: ValueError\n)') + assert repr(reader) == expected + + +def test_saveframe_reader_repr_3(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=["mod1.py::"]) + reader = SaveframeReader(filename) + + expected = ( + f'SaveframeReader(\nfilename: \'{filename}\' \n\nFrames:\nFrame 3:\n ' + f'Filename: \'{tmpdir}/{pkg_name}/mod1.py\'\n Line Number: 9\n ' + f'Function: func2\n Module: {pkg_name}.mod1\n Frame ID: \'{tmpdir}/' + f'{pkg_name}/mod1.py,9,func2\'\n Code: obj.func2()\n Variables: ' + f'[\'var1\', \'var2\', \'obj\']\n\nFrame 4:\n Filename: \'{tmpdir}/' + f'{pkg_name}/mod1.py\'\n Line Number: 14\n Function: func1\n Module: ' + f'{pkg_name}.mod1\n Frame ID: \'{tmpdir}/{pkg_name}/mod1.py,14,func1\'' + '\n Code: func2()\n Variables: [\'var1\', \'func1_var2\']\n\n' + 'Exception:\n Full String: ValueError: Error is raised\n String: ' + 'Error is raised\n Class Name: ValueError\n Qualified Name: ValueError\n)') + assert repr(reader) == expected + + +def test_saveframe_reader_str_1(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=1) + reader = SaveframeReader(filename) + + expected = ( + f'Frames:\nFrame 1:\n Filename: \'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py' + f'\'\n Line Number: 6\n Function: func3\n Module: {pkg_name}.pkg1.' + f'pkg2.mod3\n Frame ID: \'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py,6,' + 'func3\'\n Code: raise ValueError("Error is raised")\n Variables: ' + '[\'var1\', \'var2\', \'func3_var3\']\n\nException:\n Full String: ' + 'ValueError: Error is raised\n String: Error is raised\n Class Name: ' + 'ValueError\n Qualified Name: ValueError\n') + assert str(reader) == expected + + +def test_saveframe_reader_str_2(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=2) + reader = SaveframeReader(filename) + + expected = ( + f'Frames:\nFrame 1:\n Filename: \'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py' + f'\'\n Line Number: 6\n Function: func3\n Module: {pkg_name}.pkg1.' + f'pkg2.mod3\n Frame ID: \'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py,6,func3' + '\'\n Code: raise ValueError("Error is raised")\n Variables: [\'var1' + f'\', \'var2\', \'func3_var3\']\n\nFrame 2:\n Filename: \'{tmpdir}/' + f'{pkg_name}/pkg1/mod2.py\'\n Line Number: 10\n Function: mod2_cls.func2' + f'\n Module: {pkg_name}.pkg1.mod2\n Frame ID: \'{tmpdir}/{pkg_name}/' + 'pkg1/mod2.py,10,func2\'\n Code: func3()\n Variables: [\'self\', ' + '\'var1\', \'var2\']\n\nException:\n Full String: ValueError: Error is ' + 'raised\n String: Error is raised\n Class Name: ValueError\n ' + 'Qualified Name: ValueError\n') + assert str(reader) == expected + + +def test_saveframe_reader_str_3(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=["mod1.py::"]) + reader = SaveframeReader(filename) + + expected = ( + f'Frames:\nFrame 3:\n Filename: \'{tmpdir}/{pkg_name}/mod1.py\'\n Line ' + f'Number: 9\n Function: func2\n Module: {pkg_name}.mod1\n Frame ID: ' + f'\'{tmpdir}/{pkg_name}/mod1.py,9,func2\'\n Code: obj.func2()\n Variables: ' + f'[\'var1\', \'var2\', \'obj\']\n\nFrame 4:\n Filename: \'{tmpdir}/' + f'{pkg_name}/mod1.py\'\n Line Number: 14\n Function: func1\n Module: ' + f'{pkg_name}.mod1\n Frame ID: \'{tmpdir}/{pkg_name}/mod1.py,14,func1\'' + '\n Code: func2()\n Variables: [\'var1\', \'func1_var2\']\n\n' + 'Exception:\n Full String: ValueError: Error is raised\n String: ' + 'Error is raised\n Class Name: ValueError\n Qualified Name: ValueError\n') + assert str(reader) == expected + + +def test_filename(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=1) + reader = SaveframeReader(filename) + + assert reader.filename == filename + + +def test_metadata(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=1) + reader = SaveframeReader(filename) + + expected = [ + 'frame_index', 'filename', 'lineno', 'function_name', 'function_qualname', + 'function_object', 'module_name', 'code', 'frame_identifier', + 'exception_string', 'exception_full_string', 'exception_class_name', + 'exception_class_qualname', 'exception_object', 'traceback'] + assert reader.metadata == expected + + +def test_variables_1(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=2) + reader = SaveframeReader(filename) + + expected = {1: ['var1', 'var2', 'func3_var3'], 2: ['self', 'var1', 'var2']} + assert reader.variables == expected + + +def test_variables_2(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + expected = { + 1: ['var1', 'var2', 'func3_var3'], 2: ['self', 'var1', 'var2'], + 3: ['var1', 'var2', 'obj'], 4: ['var1', 'func1_var2'], 5: ['var1', 'var2']} + assert reader.variables == expected + + +def test_variables_3(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::') + reader = SaveframeReader(filename) + + expected = {3: ['var1', 'var2', 'obj'], 4: ['var1', 'func1_var2']} + assert reader.variables == expected + + +def test_get_metadata_1(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=3) + reader = SaveframeReader(filename) + + result = reader.get_metadata("filename") + expected = { + 1: f'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py', + 2: f'{tmpdir}/{pkg_name}/pkg1/mod2.py', + 3: f'{tmpdir}/{pkg_name}/mod1.py'} + assert result == expected + + result = reader.get_metadata("filename", frame_idx=2) + expected = f'{tmpdir}/{pkg_name}/pkg1/mod2.py' + assert result == expected + + +def test_get_metadata_2(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::') + reader = SaveframeReader(filename) + + result = reader.get_metadata("lineno") + expected = {3: 9, 4: 14} + assert result == expected + + result = reader.get_metadata("lineno", 4) + expected = 14 + assert result == expected + + +def test_get_metadata_3(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("function_name") + expected = {1: 'func3', 2: 'func2', 3: 'func2', 4: 'func1', 5: 'init_func1'} + assert result == expected + + result = reader.get_metadata("function_name", 5) + expected = 'init_func1' + assert result == expected + + +def test_get_metadata_4(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::..') + reader = SaveframeReader(filename) + + result = reader.get_metadata("function_qualname") + expected = {1: 'func3', 2: 'mod2_cls.func2', 3: 'func2', 4: 'func1'} + assert result == expected + + result = reader.get_metadata("function_qualname", frame_idx=2) + expected = 'mod2_cls.func2' + assert result == expected + + +def test_get_metadata_5(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + frame_idx_to_info = { + 1: {'name': 'func3', 'qualname': 'func3'}, + 2: {'name': 'func2', 'qualname': 'mod2_cls.func2'}, + 3: {'name': 'func2', 'qualname': 'func2'}, + 4: {'name': 'func1', 'qualname': 'func1'}, + 5: {'name': 'init_func1', 'qualname': 'init_func1'} + } + + result = reader.get_metadata("function_object") + assert list(result.keys()) == [1, 2, 3, 4, 5] + for key in result: + func = result[key] + name = func.__name__ + qualname = func.__qualname__ + assert name == frame_idx_to_info[key]['name'] + assert qualname == frame_idx_to_info[key]['qualname'] + + result = reader.get_metadata("function_object", 2) + assert result.__qualname__ == 'mod2_cls.func2' + + +def test_get_metadata_6(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("module_name") + expected = {1: f'{pkg_name}.pkg1.pkg2.mod3', 2: f'{pkg_name}.pkg1.mod2', + 3: f'{pkg_name}.mod1', 4: f'{pkg_name}.mod1', 5: pkg_name} + assert result == expected + + result = reader.get_metadata("module_name", 4) + assert result == f'{pkg_name}.mod1' + + +def test_get_metadata_7(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("code") + expected = {1: 'raise ValueError("Error is raised")', 2: 'func3()', + 3: 'obj.func2()', 4: 'func2()', 5: 'func1()'} + assert result == expected + + result = reader.get_metadata("code", 1) + assert result == 'raise ValueError("Error is raised")' + + result = reader.get_metadata("code", 3) + assert result == 'obj.func2()' + + +def test_get_metadata_8(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("frame_identifier") + expected = { + 1: f'{tmpdir}/{pkg_name}/pkg1/pkg2/mod3.py,6,func3', + 2: f'{tmpdir}/{pkg_name}/pkg1/mod2.py,10,func2', + 3: f'{tmpdir}/{pkg_name}/mod1.py,9,func2', + 4: f'{tmpdir}/{pkg_name}/mod1.py,14,func1', + 5: f'{tmpdir}/{pkg_name}/__init__.py,6,init_func1'} + assert result == expected + + result = reader.get_metadata("frame_identifier", frame_idx=4) + assert result == f'{tmpdir}/{pkg_name}/mod1.py,14,func1' + + result = reader.get_metadata("frame_identifier", frame_idx=2) + assert result == f'{tmpdir}/{pkg_name}/pkg1/mod2.py,10,func2' + + +def test_get_metadata_9(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("exception_string") + exepected = 'Error is raised' + assert result == exepected + + +def test_get_metadata_10(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("exception_full_string") + expected = 'ValueError: Error is raised' + assert result == expected + + +def test_get_metadata_11(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("exception_class_name") + expected = 'ValueError' + assert result == expected + + +def test_get_metadata_12(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("exception_class_qualname") + expected = 'ValueError' + assert result == expected + + +def test_get_metadata_13(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_metadata("exception_object") + assert isinstance(result, ValueError) + assert result.args == ('Error is raised',) + + +def test_get_metadata_invalid(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=2) + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_metadata("foo") + + expected = ( + "Invalid metadata requested: 'foo'. Allowed metadata entries are: " + "['frame_index', 'filename', 'lineno', 'function_name', 'function_qualname', " + "'function_object', 'module_name', 'code', 'frame_identifier', " + "'exception_string', 'exception_full_string', 'exception_class_name', " + "'exception_class_qualname', 'exception_object', 'traceback'].") + assert str(err.value) == expected + + +def test_get_metadata_invalid_frame_idx_1(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=3) + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_metadata("filename", frame_idx=4) + + expected = "Invalid value for 'frame_idx': '4'. Allowed values are: [1, 2, 3]." + assert str(err.value) == expected + + +def test_get_metadata_invalid_frame_idx_2(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::') + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_metadata("filename", frame_idx=2) + + expected = "Invalid value for 'frame_idx': '2'. Allowed values are: [3, 4]." + assert str(err.value) == expected + + +def test_get_metadata_invalid_frame_idx_3(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::') + reader = SaveframeReader(filename) + + with pytest.raises(TypeError) as err: + reader.get_metadata("filename", frame_idx='foo') + + expected = "'frame_idx' must be of type 'int', not 'str'." + assert str(err.value) == expected + + +def test_get_metadata_invalid_frame_idx_4(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::') + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_metadata("traceback", frame_idx=3) + + expected = ("'frame_idx' is not supported for querying exception metadata: " + "'traceback'.") + assert str(err.value) == expected + + +def test_get_variables_1(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_variables('var2') + expected = {1: 'blah', 2: (4, 9, 10), 3: 34, 5: 'blah'} + assert result == expected + + +def test_get_variables_2(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + module = __import__(f"{pkg_name}.pkg1.mod2", fromlist=['dummy'], level=0) + mod2_cls = getattr(module, "mod2_cls") + result = reader.get_variables('obj') + assert isinstance(result, mod2_cls) + + +def test_get_variables_3(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::') + reader = SaveframeReader(filename) + + result = reader.get_variables('var2') + expected = 34 + assert result == expected + + +def test_get_variables_4(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_variables('var2', frame_idx=2) + expected = (4, 9, 10) + assert result == expected + + +def test_get_variables_5(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_variables(['var1'], frame_idx=5) + expected = 3 + assert result == expected + + +def test_get_variables_6(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_variables(['var1', 'var2']) + expected = {1: {'var1': [4, 'foo', 2.4], 'var2': 'blah'}, + 2: {'var1': 'foo', 'var2': (4, 9, 10)}, + 3: {'var1': 'func2', 'var2': 34}, 4: {'var1': [4, 5, 2]}, + 5: {'var1': 3, 'var2': 'blah'}} + assert result == expected + + +def test_get_variables_7(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_variables(['func1_var2', 'func3_var3', 'var3']) + expected = {1: {'func3_var3': True}, 4: {'func1_var2': 4.56}} + assert result == expected + + +def test_get_variables_8(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::') + reader = SaveframeReader(filename) + + result = reader.get_variables(['func1_var2', 'func3_var3', 'var3']) + expected = {'func1_var2': 4.56} + assert result == expected + + +def test_get_variables_9(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + result = reader.get_variables(['var1', 'var2', 'func1_var2'], frame_idx=3) + expected = {'var1': 'func2', 'var2': 34} + assert result == expected + + +def test_get_variables_invalid_variable_1(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_variables('foo') + + expected = "Local variable(s) ['foo'] not found in any of the saved frames." + assert str(err.value) == expected + + +def test_get_variables_invalid_variable_2(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_variables('var2', frame_idx=4) + + expected = "Local variable(s) ['var2'] not found in the frame 4" + assert str(err.value) == expected + + +def test_get_variables_invalid_variable_3(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_variables(['var2', 'var5'], frame_idx=4) + + expected = "Local variable(s) ['var2', 'var5'] not found in the frame 4" + assert str(err.value) == expected + + +def test_get_variables_invalid_frame_idx_1(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + with pytest.raises(TypeError) as err: + reader.get_variables('var1', frame_idx='foo') + + expected = "'frame_idx' must be of type 'int', not 'str'." + assert str(err.value) == expected + + +def test_get_variables_invalid_frame_idx_2(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames=5) + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_variables('var1', frame_idx=6) + + expected = ("Invalid value for 'frame_idx': '6'. Allowed values are: " + "[1, 2, 3, 4, 5].") + assert str(err.value) == expected + + +def test_get_variables_invalid_frame_idx_3(tmpdir): + pkg_name = create_pkg(tmpdir) + filename = call_saveframe(pkg_name, tmpdir, frames='mod1.py::') + reader = SaveframeReader(filename) + + with pytest.raises(ValueError) as err: + reader.get_variables(['var1', 'var2'], frame_idx=1) + + expected = "Invalid value for 'frame_idx': '1'. Allowed values are: [3, 4]." + assert str(err.value) == expected