Skip to content

Commit

Permalink
Integrate memory-viz webstepper (#1113)
Browse files Browse the repository at this point in the history
  • Loading branch information
leowrites authored Nov 24, 2024
1 parent 0707408 commit e28ef01
Show file tree
Hide file tree
Showing 57 changed files with 47,880 additions and 110 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).
- Included the name of redundant variable in `E9959 redundant-assignment` message
- Update to pylint v3.3 and and astroid v3.3. This added support for Python 3.13 and dropped support for Python 3.8.
- Added a STRICT_NUMERIC_TYPES configuration to `python_ta.contracts` allowing to enable/disable stricter type checking of numeric types
- Added integration with MemoryViz Webstepper
- Added `z3` option to `one-iteration-checker` to only check for feasible code blocks based on edge z3 constraints

### 💫 New checkers
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies = [
"typeguard >= 4.1.0, < 5",
"wrapt >= 1.15.0, < 2",
"black",
"beautifulsoup4"
]
dynamic = ["version"]
requires-python = ">=3.9"
Expand Down
2 changes: 1 addition & 1 deletion python_ta/debug/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def snapshot(
json_compatible_vars = snapshot_to_json(variables)

# Set up command
command = ["npx", f"memory-viz@{memory_viz_version}"]
command = ["npx", f"memory-viz@{memory_viz_version}", "--width", "800"]
if memory_viz_args:
command.extend(memory_viz_args)

Expand Down
135 changes: 118 additions & 17 deletions python_ta/debug/snapshot_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

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

from bs4 import BeautifulSoup

from .snapshot import snapshot


Expand All @@ -17,20 +22,30 @@ class SnapshotTracer:
Instance attributes:
output_directory: The directory where the memory model diagrams will be saved. Defaults to the current directory.
webstepper: Opens the web-based visualizer.
_snapshots: A list of dictionaries that maps the code line number and the snapshot number.
_snapshot_args: A dictionary of keyword arguments to pass to the `snapshot` function.
_snapshot_counts: The number of snapshots taken.
_first_line: Line number of the first line in the `with` block.
"""

output_directory: Optional[str]
webstepper: bool
_snapshots: list[dict[int, int]]
_snapshot_args: dict[str, Any]
_snapshot_counts: int
_first_line: int

def __init__(self, output_directory: Optional[str] = None, **kwargs) -> None:
def __init__(
self,
output_directory: Optional[str] = None,
webstepper: bool = False,
**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.
webstepper: Opens a MemoryViz Webstepper webpage to interactively visualize the resulting memory diagrams.
**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):
Expand All @@ -39,28 +54,42 @@ def __init__(self, output_directory: Optional[str] = None, **kwargs) -> None:
raise ValueError(
"Use the output_directory parameter to specify a different output path."
)
self._snapshots = []
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 "."
self.output_directory = os.path.abspath(output_directory if output_directory else ".")
self.webstepper = webstepper
self._first_line = float("inf")

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",
),
]
if self._first_line == float("inf"):
self._first_line = frame.f_lineno
if event == "line":
filename = os.path.join(
self.output_directory,
f"snapshot-{len(self._snapshots)}.svg",
)
self._snapshot_args["memory_viz_args"].extend(["--output", filename])

snapshot(
save=True,
**self._snapshot_args,
)
self._snapshot_counts += 1

line_number = frame.f_lineno - self._first_line + 1
self._add_svg_to_map(filename, line_number)

def _add_svg_to_map(self, filename: str, line: int) -> None:
"""Add the SVG in filename to self._snapshots"""
with open(filename) as svg_file:
svg_content = svg_file.read()
self._snapshots.append(
{
"lineNumber": line,
"svg": svg_content,
}
)

def __enter__(self):
"""Set up the trace function to take snapshots at each line of code."""
Expand All @@ -70,6 +99,78 @@ def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Remove the trace function."""
"""Remove the trace function. If webstepper=True, open a Webstepper webpage."""
sys.settrace(None)
inspect.getouterframes(inspect.currentframe())[1].frame.f_trace = None
func_frame = inspect.getouterframes(inspect.currentframe())[1]
func_frame.frame.f_trace = None
if self.webstepper:
self._build_result_html(func_frame.frame)
self._open_html()

def _build_result_html(self, func_frame: types.FrameType) -> None:
"""Build and write the Webstepper html to the output directory"""
snapshot_tracer_dir = os.path.dirname(os.path.abspath(__file__))

html_content = self._read_original_html(snapshot_tracer_dir)
soup = BeautifulSoup(html_content, "html.parser")

self._modify_bundle_import_path(snapshot_tracer_dir, soup)
self._insert_data(soup, func_frame)
self._write_generated_html(soup)

def _open_html(self) -> None:
"""Open the generated HTML file in a web browser."""
index_html = f"file://{os.path.join(self.output_directory, 'index.html')}"
webbrowser.open(index_html, new=2)

def _read_original_html(self, snapshot_tracer_dir: str) -> None:
"""Read the original Webstepper html"""
original_index_html = os.path.join(snapshot_tracer_dir, "webstepper", "index.html")
with open(original_index_html, "r") as file:
html_content = file.read()
return html_content

def _modify_bundle_import_path(self, snapshot_tracer_dir: str, soup: BeautifulSoup) -> None:
"""Modify the bundle path to the absolute path to the bundle"""
original_js_bundle = os.path.join(snapshot_tracer_dir, "webstepper", "index.bundle.js")
soup.select("script")[0]["src"] = f"file://{original_js_bundle}"

def _insert_data(self, soup: BeautifulSoup, func_frame: types.FrameType) -> None:
"""Insert the SVG array and code string into the Webstepper index HTML."""
insert_script = (
f"<script>window.codeText=`{self._get_code(func_frame)}` </script>\n"
+ f"<script>window.svgArray={json.dumps(self._snapshots)}</script>\n"
)
soup.select("script")[0].insert_before(BeautifulSoup(insert_script, "html.parser"))

def _write_generated_html(self, soup: BeautifulSoup) -> None:
"""Write the generated Webstepper html to the output directory"""
modified_index_html = os.path.join(self.output_directory, "index.html")
with open(modified_index_html, "w") as file:
file.write(str(soup))

def _get_code(self, func_frame: types.FrameType) -> str:
"""Retrieve and save the code string to be displayed in Webstepper."""
code_lines = inspect.cleandoc(inspect.getsource(func_frame))
i = self._first_line - func_frame.f_code.co_firstlineno
lst_str_lines = code_lines.splitlines()
num_whitespace = len(lst_str_lines[i]) - len(lst_str_lines[i].lstrip())

endpoint = len(lst_str_lines)
startpoint = i
while i < len(lst_str_lines):
line = lst_str_lines[i]
if (
line.strip() != ""
and not line.lstrip()[0] == "#"
and not line[:num_whitespace].isspace()
):
break
if line.lstrip() != "" and len(line) - len(line.lstrip()) >= num_whitespace:
lst_str_lines[i] = line[num_whitespace:]
else:
lst_str_lines[i] = line.lstrip()
endpoint = i
i += 1

return "\n".join(lst_str_lines[startpoint : endpoint + 1])
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.
47,053 changes: 47,053 additions & 0 deletions python_ta/debug/webstepper/index.bundle.js

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions python_ta/debug/webstepper/index.bundle.js.LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @mui/styled-engine v6.0.1
*
* @license MIT
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
1 change: 1 addition & 0 deletions python_ta/debug/webstepper/index.bundle.js.map

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions python_ta/debug/webstepper/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>MemoryViz Webstepper</title>
<script defer="defer" src="index.bundle.js"></script>
</head>

<body>
<div id="root"></div>
</body>
</html>
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
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

0 comments on commit e28ef01

Please sign in to comment.