Skip to content

Commit

Permalink
Add optional initialization argument output to AccumulationTable (#988
Browse files Browse the repository at this point in the history
)
  • Loading branch information
AinaMerch authored Dec 10, 2023
1 parent 9287900 commit 42cb284
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions docs/debug/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 20 additions & 10 deletions python_ta/debug/accumulation_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,15 +65,17 @@ 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]
"""A dictionary mapping loop accumulator variable name to its values across all loop iterations."""
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:
Expand All @@ -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"""
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_config/test_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
def some_function():
x = 5 # pylint: disable something
y = 0
result = x / y
return result


a = 10
b = 20
59 changes: 59 additions & 0 deletions tests/test_debug/test_accumulation_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

0 comments on commit 42cb284

Please sign in to comment.