Skip to content

Commit

Permalink
add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
leowrites committed Sep 28, 2024
1 parent 5156762 commit 9428878
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 20 deletions.
49 changes: 45 additions & 4 deletions docs/debug/index.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Loop and Recursion Tracing
# Debugging with Tracing

This page describes an additional PythonTA feature: print-based debugging for loops and recursion.
This feature makes tracing easier by printing the state of each loop iteration or recursive function call in a nicely-formatted table using the [tabulate] library.
This functionality is found in the `python_ta.debug` submodule.
This page describes two additional PythonTA features:

1. Print-based debugging for loops and recursion
1. This feature makes tracing easier by printing the state of each loop iteration or recursive function call in a nicely-formatted table using the [tabulate] library.
2. Memory debugging through by generaing the Python memory model.
1. This feature makes it easier to visualize the Python memory model by leveraging the [`MemoryViz`](https://github.com/david-yz-liu/memory-viz) library.

These additional features are found in the `python.debug` submodule.

## Loop tracing with `AccumulationTable`

Expand Down Expand Up @@ -250,3 +255,39 @@ The `RecursionTable` is a new PythonTA feature and currently has the following k

[tabulate]: https://github.com/astanin/python-tabulate
[`sys.settrace`]: https://docs.python.org/3/library/sys.html#sys.settrace

## Tracing the Python Memory Model

The following section will focus on tracing the Python memory model. This feature uses the `python_ta.debug.SnapshotManager` as a context manager.

### Example usage

```python
# demo.py
from python_ta.debug import SnapshotManager

def func_multi_line(output_path=None) -> None:
with SnapshotManager(output_directory=output_path, include=("func_multi_line",)):
num = 123
some_string = "Hello, world"
num2 = 321
arr = [some_string, "string 123321"]

if __name__ == '__main__':
func_multi_line()
```

When this is run, variables within the `func_multi_line` function are captured, and memory models are outputted to the calling directory for every line of code. For the expected output of this function, see the snapshots in `tests/test_debug/snapshot_manager_testing_snapshots/func_multi_line`.

### API

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

### Current Limitations

The `SnapshotManager` has the following limitations:

1. The context manager does not step into any function calls. Calling functions inside the function may lead to undefined behaviors.
2. `SnapshotManager` uses [`sys.settrace`] to update variable state, and so is not compatible with other libraries (e.g. debuggers, code coverage tools).
28 changes: 19 additions & 9 deletions python_ta/debug/snapshot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,34 @@ class SnapshotManager:
Class used to manage the snapshots taken during the execution of a program.
Instance attributes:
memory_viz_args: The arguments to pass to the memory visualizer
memory_viz_version: The version of the memory visualizer to use
memory_viz_args: The arguments to pass to the MemoryViz CLI
memory_viz_version: The version of MemoryViz to use
include: A collection of function names, either as strings or regular expressions, whose variables will be captured
output_filepath: The path to save the snapshots
output_directory: The path to save the snapshots
"""

memory_viz_args: Optional[list[str]]
memory_viz_version: str
_snapshot_counts: int
include: Optional[Iterable[str | re.Pattern]]
output_filepath: Optional[str]
output_directory: Optional[str]

def __init__(
self,
memory_viz_args: Optional[list[str]] = None,
memory_viz_version: str = "latest",
include: Optional[Iterable[str | re.Pattern]] = None,
output_filepath: Optional[str] = None,
output_directory: Optional[str] = None,
) -> None:
"""Initialize a SnapshotManager context manager for snapshot-based debugging."""
"""Initialize a SnapshotManager context manager for snapshot-based debugging.
Args:
memory_viz_args: The arguments to pass to the memory visualizer.
memory_viz_version: The version of the memory visualizer to use
include: A collection of function names, either as strings or regular expressions, whose variables will be captured
output_directory: The path to save the snapshots. Use this argument instead of the `--output` flag to specify the output directory.
Defaults to the current directory.
"""
if memory_viz_args is None:
memory_viz_args = ["--roughjs-config", "seed=12345"]
self.memory_viz_version = memory_viz_version
Expand All @@ -43,9 +51,11 @@ def __init__(
self.include = include

if any("--output" in arg for arg in memory_viz_args):
raise ValueError("Use the output_filepath argument to specify a different output path.")
raise ValueError(
"Use the output_directory argument to specify a different output path."
)

self.output_filepath = output_filepath if output_filepath else "."
self.output_directory = output_directory if output_directory else "."

def _trace_func(self, frame: types.FrameType, event: str, _arg: Any) -> None:
"""Trace function to take snapshots at each line of code."""
Expand All @@ -55,7 +65,7 @@ def _trace_func(self, frame: types.FrameType, event: str, _arg: Any) -> None:
[
"--output",
os.path.join(
os.path.abspath(self.output_filepath),
os.path.abspath(self.output_directory),
f"snapshot-{self._snapshot_counts}.svg",
),
]
Expand Down
14 changes: 7 additions & 7 deletions tests/test_debug/test_snapshot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,33 @@


def func_one_line(output_path=None) -> None:
with SnapshotManager(output_filepath=output_path, include=("func_one_line",)):
with SnapshotManager(output_directory=output_path, include=("func_one_line",)):
num = 123


def func_multi_line(output_path=None) -> None:
with SnapshotManager(output_filepath=output_path, include=("func_multi_line",)):
with SnapshotManager(output_directory=output_path, include=("func_multi_line",)):
num = 123
some_string = "Hello, world"
num2 = 321
arr = [some_string, "string 123321"]


def func_mutation(output_path=None) -> None:
with SnapshotManager(output_filepath=output_path, include=("func_mutation",)):
with SnapshotManager(output_directory=output_path, include=("func_mutation",)):
num = 123
num = 321


def func_for_loop(output_path=None) -> None:
with SnapshotManager(output_filepath=output_path, include=("func_for_loop",)):
with SnapshotManager(output_directory=output_path, include=("func_for_loop",)):
nums = [1, 2, 3]
for i in range(len(nums)):
nums[i] = nums[i] + 1


def func_if_else(output_path=None) -> None:
with SnapshotManager(output_filepath=output_path, include=("func_if_else",)):
with SnapshotManager(output_directory=output_path, include=("func_if_else",)):
num = 10
if num > 5:
result = "greater"
Expand All @@ -52,7 +52,7 @@ def func_if_else(output_path=None) -> None:


def func_while(output_path=None) -> None:
with SnapshotManager(output_filepath=output_path, include=("func_while",)):
with SnapshotManager(output_directory=output_path, include=("func_while",)):
num = 0
while num < 3:
num += 1
Expand Down Expand Up @@ -139,7 +139,7 @@ def test_outputs_to_default_directory_with_no_memory_viz_args(snapshot):
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires Python 3.10 or higher")
def test_using_output_flag():
with pytest.raises(
ValueError, match="Use the output_filepath argument to specify a different output path."
ValueError, match="Use the output_directory argument to specify a different output path."
):
with SnapshotManager(
include=("func_duplicate_output_path",), memory_viz_args=["--output", "."]
Expand Down

0 comments on commit 9428878

Please sign in to comment.