Skip to content

Commit

Permalink
accumulation table: Improve documentation and use copy.copy for objec…
Browse files Browse the repository at this point in the history
…ts (#834)
  • Loading branch information
david-yz-liu authored Aug 8, 2022
1 parent c1b8720 commit ddbbffb
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 61 deletions.
128 changes: 83 additions & 45 deletions docs/debug/index.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,108 @@
# PythonTA Additional Debugging Features
# Loop Debugging

This page describes in greater detail the additional debugging features that
PythonTa has to offer. If anything is unclear, incorrect, or missing, please don't hesitate to send an
email to \[david at cs dot toronto dot edu\].
This page describes an additional PythonTA feature: print-based loop debugging.
This feature makes it easier to trace the execution of a for loop by printing the state of each loop iteration in a nicely-formatted table using the [tabulate] library.

## Printed Accumulation Table
## Example usage

This feature allows the output of each loop iteration to be printed
nicely in table format.

For example:
This feature uses the `python_ta.debug.AccumulationTable` as a context manager wrapping a for loop.
Here is a complete example of its use:

```python
def my_func(numbers: list) -> tuple:
"""Return the sum and average at each iteration
of the numbers in numbers.
# demo.py
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 = 'N/A'
for number in numbers:
sum_so_far = sum_so_far + number
avg_so_far = sum(list_so_far) / len(list_so_far)
list_so_far.append(avg_so_far)
avg_so_far = None
with AccumulationTable(["sum_so_far", "avg_so_far", "list_so_far"]):
for number in numbers:
sum_so_far = sum_so_far + number
avg_so_far = sum(list_so_far) / len(list_so_far)
list_so_far.append((sum_so_far, avg_so_far))

return list_so_far


return (sum_so_far, list_so_far)
if __name__ == '__main__':
calculate_sum_and_averages([10, 20, 30, 40, 50, 60])
```

Table output:
When this file is run, we get the following output:

```console
$ python demo.py
iteration loop variable (number) sum_so_far avg_so_far list_so_far
----------- ------------------------ ------------ ------------ ---------------------------------------------------------------------------
0 N/A 0 []
1 10 10 10 [(10, 10.0)]
2 20 30 15 [(10, 10.0), (30, 15.0)]
3 30 60 20 [(10, 10.0), (30, 15.0), (60, 20.0)]
4 40 100 25 [(10, 10.0), (30, 15.0), (60, 20.0), (100, 25.0)]
5 50 150 30 [(10, 10.0), (30, 15.0), (60, 20.0), (100, 25.0), (150, 30.0)]
6 60 210 35 [(10, 10.0), (30, 15.0), (60, 20.0), (100, 25.0), (150, 30.0), (210, 35.0)]
```
iterations loop variable (number) sum_so_far list_so_far avg_so_far
------------ ------------------------ ------------ ------------------------ ------------
0 N/A 0 [] N/A
1 10 10 [10] 10.0
2 20 30 [10, 20] 15.0
3 30 60 [10, 20, 30] 20.0
4 40 100 [10, 20, 30, 40] 25.0
5 50 150 [10, 20, 30, 40, 50] 30.0
6 60 210 [10, 20, 30, 40, 50, 60] 35.0

## API

```{eval-rst}
.. automethod:: python_ta.debug.AccumulationTable.__init__
```

### Usage Guide
The `AccumulationTable` class has the following instance attributes you can access after the `with` statement.

Over any accumulator loop, such as:
```{eval-rst}
.. autoattribute:: python_ta.debug.AccumulationTable.loop_var_name
```python
my_list = [10, 20, 30]
sum_so_far = 0
for number in my_list:
sum_so_far = sum_so_far + number
.. autoattribute:: python_ta.debug.AccumulationTable.loop_var_val
.. autoattribute:: python_ta.debug.AccumulationTable.loop_accumulators
```

Add the call `with AccumulationTable():` above the accumulator loop
with everything in the scope of the loop nested in the call. Within
the parameter for the initialization of `AccumulationTable()` use a
list of strings containing all the accumulator variables as an argument
that need to be tracked. For example:
For example:

```python
from python_ta.debug import AccumulationTable

my_list = [10, 20, 30]
sum_so_far = 0
with AccumulationTable(['sum_so_far']):
for number in my_list:
sum_so_far = sum_so_far + number

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
with AccumulationTable(["sum_so_far", "avg_so_far", "list_so_far"]) as table:
for number in numbers:
sum_so_far = sum_so_far + number
avg_so_far = sum(list_so_far) / len(list_so_far)
list_so_far.append((sum_so_far, avg_so_far))

print(table.loop_accumulators)
return list_so_far

```

## Current limitations

The `AccumulationTable` is a new PythonTA feature and currently has the following known limitations:

1. `AccumulationTable` uses [`sys.settrace`] to update variable state, and so is not compatible with other libraries (e.g. debuggers, code coverage tools).
2. Only supports for loops with one target. E.g. loops like `for i, item in enumerate(my_list)` are not supported.
3. Only supports loop accumulation variables, but not accumulators as part of an object.
For example, instance attribute accumulators are not supported:

```python
def update_my_sum(self, numbers):
for number in numbers:
self.sum_so_far = self.sum_so_far + number
```

4. Loop variable state is stored by creating shallow copies of the objects.
Loops that mutate a nested part of an object will not have their state displayed properly.

[tabulate]: https://github.com/astanin/python-tabulate
[`sys.settrace`]: https://docs.python.org/3/library/sys.html#sys.settrace
18 changes: 14 additions & 4 deletions python_ta/debug/accumulation_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
"""
from __future__ import annotations

import copy
import inspect
import sys
import types
from typing import Any

import astroid
import tabulate
Expand Down Expand Up @@ -60,15 +62,23 @@ class AccumulationTable:
loop_var_name: the name of the loop variable
loop_var_val: the values of the loop variable during each iteration
_loop_lineno: the line number of the for loop
"""

loop_accumulators: dict[str, list]
"""A dictionary mapping loop accumulator variable name to its values across all loop iterations."""
loop_var_name: str
"""The name of the for loop target variable."""
loop_var_val: list
"""The values of the for loop target variable across all loop iterations."""
_loop_lineno: int

def __init__(self, accumulation_names: list[str]) -> None:
"""Initialize an Accumulation Table context manager for print-based loop debugging.
Args:
accumulation_names: a list of the loop accumulator variable names to display.
"""
self.loop_accumulators = {accumulator: [] for accumulator in accumulation_names}
self.loop_var_name = ""
self.loop_var_val = []
Expand All @@ -77,13 +87,13 @@ def __init__(self, accumulation_names: list[str]) -> None:
def _record_iteration(self, frame: types.FrameType) -> None:
"""Record the values of the accumulator variables and loop variable of an iteration"""
if len(self.loop_var_val) > 0:
self.loop_var_val.append(frame.f_locals[self.loop_var_name])
self.loop_var_val.append(copy.copy(frame.f_locals[self.loop_var_name]))
else:
self.loop_var_val.append("N/A")

for accumulator in self.loop_accumulators:
if accumulator in frame.f_locals:
self.loop_accumulators[accumulator].append(frame.f_locals[accumulator])
self.loop_accumulators[accumulator].append(copy.copy(frame.f_locals[accumulator]))
else:
raise NameError

Expand All @@ -106,7 +116,7 @@ def _tabulate_data(self) -> None:
)
)

def _trace_loop(self, frame: types.FrameType, event: str, arg: Any) -> None:
def _trace_loop(self, frame: types.FrameType, event: str, _arg: Any) -> None:
"""Trace through the for loop and store the values of the
accumulators and loop variable during each iteration
"""
Expand Down
26 changes: 14 additions & 12 deletions sample_usage/print_table.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
"""Examples for the python_ta.debug.AccumulationTable class.
"""
Examples of how to use the AccumulationTable Class
"""
from __future__ import annotations

import inspect
from typing import Union

from python_ta.debug.accumulation_table import AccumulationTable
from python_ta.debug import AccumulationTable


def my_func(numbers: list) -> None:
"""Print the Accumulation Table to show the tracking of the respective variables."""
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 = "N/A"
with AccumulationTable(["sum_so_far", "list_so_far", "avg_so_far"]):
avg_so_far = None
with AccumulationTable(["sum_so_far", "avg_so_far", "list_so_far"]):
for number in numbers:
sum_so_far = sum_so_far + number
list_so_far = list_so_far + [number]
avg_so_far = sum(list_so_far) / len(list_so_far)
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


class Restaurant:
Expand Down Expand Up @@ -53,8 +54,9 @@ def print_total(self) -> None:


if __name__ == "__main__":
print("my_func function example:")
my_func([10, 20, 30, 40, 50, 60])
print("calculate_sum_and_averages([10, 20, 30, 40, 50, 60])")
calculate_sum_and_averages([10, 20, 30, 40, 50, 60])
print("")

print("\nRestaurant class example:")
restaurant = Restaurant()
Expand Down

0 comments on commit ddbbffb

Please sign in to comment.