diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fab216be..7f3f5f657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `AccumulationTable` can now track variables initialized within the `for` loop. Prior, only variables initialized before the `for` loop could be tracked. - `AccumulationTable` now stores deep copies of objects rather than shallow copies, thus fixing issues that come up in case of mutation during loop. - `AccumulationTable` can now take in any accumulator expressions, for eg. `x * 2`, instead of just variables. +- `AccumulationTable` now has an optional initialization argument `output` which allows the users to choose whether they want to write the Accumulation Table to a file. ### Bug fixes diff --git a/docs/debug/index.md b/docs/debug/index.md index 932721c94..90f2c2f59 100644 --- a/docs/debug/index.md +++ b/docs/debug/index.md @@ -128,6 +128,29 @@ def calculate_sum_and_averages(numbers: list) -> list: ``` +You also have the option to pass in a file path as an attribute to the AccumulationTable object. In this case, the table will be appended to the file instead of being written the console. +For example: + +```python +from python_ta.debug import AccumulationTable + + +def calculate_sum_and_averages(numbers: list) -> list: + """Return the running sums and averages of the given numbers. + """ + sum_so_far = 0 + list_so_far = [] + avg_so_far = None + output_file = 'output.txt' + with AccumulationTable(["sum_so_far", "avg_so_far", "list_so_far"], output_file) as table: + for number in numbers: + sum_so_far = sum_so_far + number + avg_so_far = sum_so_far / (len(list_so_far) + 1) + list_so_far.append((sum_so_far, avg_so_far)) + + return list_so_far +``` + ## Current limitations The `AccumulationTable` is a new PythonTA feature and currently has the following known limitations: diff --git a/python_ta/debug/accumulation_table.py b/python_ta/debug/accumulation_table.py index 985fb3ab3..91b620980 100644 --- a/python_ta/debug/accumulation_table.py +++ b/python_ta/debug/accumulation_table.py @@ -8,7 +8,7 @@ import inspect import sys import types -from typing import Any, Union +from typing import Any, Optional, Union import astroid import tabulate @@ -65,6 +65,7 @@ class AccumulationTable: loop_variables: a mapping between the loop variables and their values during each iteration _loop_lineno: the line number of the loop + output_filepath: the filepath where the table will be written if it is passed in, defaults to None """ loop_accumulators: dict[str, list] @@ -72,8 +73,9 @@ class AccumulationTable: loop_variables: dict[str, list] """A dictionary mapping loop variable variable name to its values across all loop iterations.""" _loop_lineno: int + output_filepath: Optional[str] - def __init__(self, accumulation_names: list[str]) -> None: + def __init__(self, accumulation_names: list[str], output: Union[None, str] = None) -> None: """Initialize an AccumulationTable context manager for print-based loop debugging. Args: @@ -83,6 +85,7 @@ def __init__(self, accumulation_names: list[str]) -> None: self.loop_accumulators = {accumulator: [] for accumulator in accumulation_names} self.loop_variables = {} self._loop_lineno = 0 + self.output_filepath = output def _record_iteration(self, frame: types.FrameType) -> None: """Record the values of the accumulator variables and loop variables of an iteration""" @@ -124,15 +127,22 @@ def _create_iteration_dict(self) -> dict: def _tabulate_data(self) -> None: """Print the values of the accumulator and loop variables into a table""" iteration_dict = self._create_iteration_dict() - print( - tabulate.tabulate( - iteration_dict, - headers="keys", - colalign=(*["left"] * len(iteration_dict),), - disable_numparse=True, - missingval="None", - ) + table = tabulate.tabulate( + iteration_dict, + headers="keys", + colalign=(*["left"] * len(iteration_dict),), + disable_numparse=True, + missingval="None", ) + if self.output_filepath is None: + print(table) + else: + try: + with open(self.output_filepath, "a") as file: + file.write(table) + file.write("\n") + except OSError as e: + print(f"Error writing to file: {e}") def _trace_loop(self, frame: types.FrameType, event: str, _arg: Any) -> None: """Trace through the loop and store the values of the diff --git a/tests/test_config/test_file.py b/tests/test_config/test_file.py new file mode 100644 index 000000000..b41f62933 --- /dev/null +++ b/tests/test_config/test_file.py @@ -0,0 +1,9 @@ +def some_function(): + x = 5 # pylint: disable something + y = 0 + result = x / y + return result + + +a = 10 +b = 20 diff --git a/tests/test_debug/test_accumulation_table.py b/tests/test_debug/test_accumulation_table.py index 3eceb9ccd..336b23ac0 100644 --- a/tests/test_debug/test_accumulation_table.py +++ b/tests/test_debug/test_accumulation_table.py @@ -3,8 +3,10 @@ types of accumulator loops """ import copy +import shutil import pytest +import tabulate from python_ta.debug import AccumulationTable from python_ta.debug.snapshot import snapshot @@ -477,3 +479,60 @@ def test_snapshot_three_levels() -> None: } } == local_vars[1] assert {"func3": {"i": 4, "test_var1c": [0, 1, 2, 3, 4]}} == local_vars[2] + + +def test_output_to_existing_file(tmp_path) -> None: + test_list = [10, 20, 30] + sum_so_far = 0 + output_file = tmp_path / "output.txt" + with open(output_file, "a") as file: + file.write("Existing Content") + file.write("\n") + with AccumulationTable(["sum_so_far"], output=str(output_file)) as table: + for number in test_list: + sum_so_far = sum_so_far + number + iteration_dict = table._create_iteration_dict() + + assert table.loop_variables == {"number": ["N/A", 10, 20, 30]} + assert table.loop_accumulators == {"sum_so_far": [0, 10, 30, 60]} + + with open(output_file, "r") as file: + content = file.read() + expected_values = tabulate.tabulate( + iteration_dict, + headers="keys", + colalign=(*["left"] * len(iteration_dict),), + disable_numparse=True, + missingval="None", + ) + assert "Existing Content" in content + assert expected_values in content + + shutil.rmtree(tmp_path) + + +def test_output_to_new_file(tmp_path) -> None: + test_list = [10, 20, 30] + sum_so_far = 0 + with AccumulationTable(["sum_so_far"], output=str(tmp_path / "output.txt")) as table: + for number in test_list: + sum_so_far = sum_so_far + number + iteration_dict = table._create_iteration_dict() + + output_file = tmp_path / "output.txt" + assert output_file.exists() + assert table.loop_variables == {"number": ["N/A", 10, 20, 30]} + assert table.loop_accumulators == {"sum_so_far": [0, 10, 30, 60]} + + with open(output_file, "r") as file: + content = file.read() + expected_values = tabulate.tabulate( + iteration_dict, + headers="keys", + colalign=(*["left"] * len(iteration_dict),), + disable_numparse=True, + missingval="None", + ) + assert expected_values in content + + shutil.rmtree(tmp_path)