Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Snapshot Tracer #1086

Merged
merged 73 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
3f5548a
add simple test cases
leowrites Sep 15, 2024
88c7c47
add context manager to output snapshots for the immediate function stack
leowrites Sep 15, 2024
ead317e
output test results to a temp folder and compare results against expe…
leowrites Sep 16, 2024
bc02ecd
make snapshot manager return a count
leowrites Sep 16, 2024
f0f518d
output snapshots immediately
leowrites Sep 17, 2024
56558fc
Merge branch 'master' of https://github.com/pyta-uoft/pyta into snaps…
leowrites Sep 22, 2024
a34f48f
Merge branch 'master' of https://github.com/pyta-uoft/pyta into snaps…
leowrites Sep 22, 2024
566f529
clean up tests and add include arg to context manager
leowrites Sep 22, 2024
1fce042
experiment with outputting snapshots
leowrites Sep 22, 2024
a4e20b5
take snapshots using a flag and add initial snapshots
leowrites Sep 23, 2024
ea956ee
Use snapshot(save)
leowrites Sep 23, 2024
286a148
Remove duplicated snapshots
leowrites Sep 23, 2024
aaace6e
add snapshot results for "for" loop and "while" loop
leowrites Sep 23, 2024
eabb2b5
fix memory viz arguments not being correctly passed and update snapshots
leowrites Sep 23, 2024
55b738a
Delete snapshots
leowrites Sep 24, 2024
5cf45f7
Use pytest-snapshot to take snapshots
leowrites Sep 24, 2024
5c1f4e5
Don't run precommit hook on snapshots
leowrites Sep 24, 2024
0f72af2
Don't run precommit hook on snapshots
leowrites Sep 24, 2024
52c2793
Only run tests for python 3.10 or higher
leowrites Sep 25, 2024
19b7336
Add spaces
leowrites Sep 25, 2024
39fbdf5
read the directory to match the files
leowrites Sep 25, 2024
b156ea1
check for output argument and raise error if there is a duplicate
leowrites Sep 25, 2024
9b1a7f9
update snapshots
leowrites Sep 25, 2024
7d52752
add if else snapshots
leowrites Sep 25, 2024
15c5491
Fix typing and remove unused function
leowrites Sep 25, 2024
37c5b94
Fix version not passed correctly and don't let user use the --output …
leowrites Sep 25, 2024
3af5242
Add tests and extract common set up function
leowrites Sep 25, 2024
aadbbcc
add some wait time before checking for files
leowrites Sep 25, 2024
c88790d
increase timer
leowrites Sep 25, 2024
1ada687
remove test
leowrites Sep 25, 2024
a6d2f1f
change test order
leowrites Sep 25, 2024
918c3bf
add debugging statements
leowrites Sep 26, 2024
5db8a03
try abs paths
leowrites Sep 26, 2024
1f61790
try to specify memory viz version
leowrites Sep 26, 2024
f7456af
add debugging statement
leowrites Sep 26, 2024
71880dd
ignore snapshot manager when generating report
leowrites Sep 27, 2024
ea4f7c1
add documentation to snapshot manager
leowrites Sep 28, 2024
b39a3f8
remove comments and debug statements
leowrites Sep 28, 2024
decdb55
Add to CHANGELOG.md
leowrites Sep 28, 2024
5156762
add back unused args
leowrites Sep 28, 2024
9428878
add documentation
leowrites Sep 28, 2024
088ae69
limit python verion and fix tests
leowrites Sep 28, 2024
4a7ff3a
Use fixture for setup and teardown
leowrites Sep 28, 2024
6923f9a
Remove generated svg
leowrites Sep 28, 2024
bd7a013
Fix typo and improve clarity
leowrites Sep 28, 2024
fe8423d
Remove waiting for files to be generated and import logging
leowrites Sep 28, 2024
01bfc38
Update documentation
leowrites Sep 28, 2024
0da8f4a
Clean up documentation
leowrites Sep 28, 2024
7f7969e
Code refactor and update snapshots
leowrites Sep 28, 2024
0dabd4d
Make memory viz version optional and use correct name
leowrites Sep 28, 2024
db51434
Reorganize test file
leowrites Sep 30, 2024
82fd241
use kwargs
leowrites Sep 30, 2024
50b3166
Update file name to snapshot_tracer.py
leowrites Sep 30, 2024
9bfc438
add docstring
leowrites Sep 30, 2024
543f43b
update expected snapshot directory
leowrites Sep 30, 2024
e5b0609
fix test
leowrites Sep 30, 2024
659cfbd
group tests in a class
leowrites Sep 30, 2024
e1cbbcb
Fix instances of snapshot manager remaining
leowrites Sep 30, 2024
2ab72c7
clean up instance attribute documentation
leowrites Sep 30, 2024
6bc2024
remove useless test
leowrites Sep 30, 2024
17d4451
Merge branch 'master' of https://github.com/pyta-uoft/pyta into snaps…
leowrites Oct 2, 2024
4eaf93c
rename include to include_frames
leowrites Oct 2, 2024
c6dd316
Update tests to use exclude_vars
leowrites Oct 2, 2024
7e0c0b3
add check for case when there are no variables in the frame
leowrites Oct 2, 2024
b313540
update snapshots
leowrites Oct 2, 2024
9bcc714
remove fixture and hard coded test directory
leowrites Oct 2, 2024
c7a4018
remove length checks
leowrites Oct 2, 2024
05f6f99
remove check
leowrites Oct 2, 2024
2f8c3af
Make documentation more clear
leowrites Oct 2, 2024
c7eab67
Clean up doc
leowrites Oct 2, 2024
8c93f21
remove unused test and rename test function
leowrites Oct 2, 2024
9a6cd12
delete snapshot
leowrites Oct 2, 2024
2c69d7d
fix memory viz version
leowrites Oct 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ jobs:
pip install -e .[dev,cfg,z3]
- name: Run tests
run: |
pytest -vv --cov python_ta --cov-config=.coveragerc --cov-report lcov --ignore=tests/test_type_constraints --ignore=tests/test_type_inference tests --ignore=tests/test_debug/test_accumulation_table.py --ignore=tests/test_debug/test_recursion_table.py
pytest -vv tests/test_debug/test_accumulation_table.py tests/test_debug/test_recursion_table.py
pytest -vv --cov python_ta --cov-config=.coveragerc --cov-report lcov --ignore=tests/test_type_constraints --ignore=tests/test_type_inference tests --ignore=tests/test_debug/test_accumulation_table.py --ignore=tests/test_debug/test_recursion_table.py --ignore=tests/test_debug/test_snapshot_tracer.py
pytest -vv tests/test_debug/test_accumulation_table.py tests/test_debug/test_recursion_table.py tests/test_debug/test_snapshot_tracer.py
- name: Upload test coverage to coveralls.io
uses: coverallsapp/github-action@v2.3.0
env:
Expand Down
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ exclude: |
(?x)^(
examples|
tests/fixtures/sample_dir|
tests/test_debug/snapshot_testing_snapshots
tests/test_debug/snapshot_testing_snapshots|
tests/test_debug/snapshot_tracer_testing_snapshots
)

ci:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- Added `include_frames` filter to `snapshot`
- Added `exclude_vars` filter to `snapshot`
- Added new `python_ta.debug` module with an `SnapshotTracer` context manager for generating memory models

### 💫 New checkers

Expand Down
58 changes: 54 additions & 4 deletions docs/debug/index.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Loop and Recursion Tracing
# Loop, Recursion and Memory 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: 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. Snapshot-based debugging for visualizing the memory diagram: 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 +253,50 @@ 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.SnapshotTracer` as a context manager to visualize program memory.

### Example usage

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


def func_multi_line(output_directory: str = None) -> None:
"""
Function for testing SnapshotTracer
"""
with SnapshotTracer(
output_directory=output_directory,
include_frames=("func_multi_line",),
exclude_vars=("output_directory",),
memory_viz_args=MEMORY_VIZ_ARGS,
):
num = 123
some_string = "Hello, world"
num2 = 321
arr = [some_string, "string 123321"]


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

When this function runs, the variables within `func_multi_line` are captured, and memory models are outputted to `output_directory` for each line of code. For the expected output, refer to the snapshots in `tests/test_debug/snapshot_tracer_testing_snapshots/func_multi_line`.

### API

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

### Current Limitations

The `SnapshotTracer` has the following limitations:

1. Due to differences in Python interpreters, this context manager only works with Python versions >= 3.10.
2. The context manager does not step into any function calls. Calling functions within the traced function may lead to undefined behavior.
3. `SnapshotTracer` uses [`sys.settrace`] to update variable states, and therefore is not compatible with other libraries (e.g., debuggers, code coverage tools).
1 change: 1 addition & 0 deletions python_ta/debug/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .accumulation_table import AccumulationTable
from .recursion_table import RecursionTable
from .snapshot_tracer import SnapshotTracer
75 changes: 75 additions & 0 deletions python_ta/debug/snapshot_tracer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import copy
import inspect
import logging
import os
import sys
import types
from typing import Any, Optional

from .snapshot import snapshot


class SnapshotTracer:
"""
A class used for snapshot-based debugging to visualize program memory at each line in the calling function.

Instance attributes:
output_directory: The directory where the memory model diagrams will be saved. Defaults to the current directory.
_snapshot_args: A dictionary of keyword arguments to pass to the `snapshot` function.
_snapshot_counts: The number of snapshots taken.
"""

output_directory: Optional[str]
_snapshot_args: dict[str, Any]
_snapshot_counts: int

def __init__(self, output_directory: Optional[str] = None, **kwargs) -> None:
"""Initialize a context manager for snapshot-based debugging.

Args:
output_directory: The directory to save the snapshots, defaulting to the current directory.
**Note**: Use this argument instead of the `--output` flag in `memory_viz_args` to specify the output directory.
**kwargs: All other keyword arguments are passed to `python.debug.snapshot`. Refer to the `snapshot` function for more details.
"""
if sys.version_info < (3, 10, 0):
logging.warning("You need Python 3.10 or later to use SnapshotTracer.")
if any("--output" in arg for arg in kwargs.get("memory_viz_args", [])):
raise ValueError(
"Use the output_directory parameter to specify a different output path."
)
self._snapshot_args = kwargs
self._snapshot_args["memory_viz_args"] = copy.deepcopy(kwargs.get("memory_viz_args", []))
self._snapshot_counts = 0
self.output_directory = output_directory if output_directory else "."

def _trace_func(self, frame: types.FrameType, event: str, _arg: Any) -> None:
"""Take a snapshot of the variables in the functions specified in `self.include`"""
if event == "line" and frame.f_locals:
self._snapshot_args["memory_viz_args"].extend(
[
"--output",
os.path.join(
os.path.abspath(self.output_directory),
f"snapshot-{self._snapshot_counts}.svg",
),
]
)
snapshot(
save=True,
**self._snapshot_args,
)
self._snapshot_counts += 1

def __enter__(self):
"""Set up the trace function to take snapshots at each line of code."""
func_frame = inspect.getouterframes(inspect.currentframe())[1].frame
func_frame.f_trace = self._trace_func
sys.settrace(lambda *_args: None)
return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Remove the trace function."""
sys.settrace(None)
inspect.getouterframes(inspect.currentframe())[1].frame.f_trace = None
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading