From 11d33309498116bdb9102b2f80c84853052c23a4 Mon Sep 17 00:00:00 2001 From: YusufT0 Date: Wed, 9 Sep 2026 16:58:33 +0300 Subject: [PATCH 1/2] fix: fixed the #154 bugs. --- graphtage/ast.py | 2 +- graphtage/pydiff.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphtage/ast.py b/graphtage/ast.py index aeabb13..6c87f58 100644 --- a/graphtage/ast.py +++ b/graphtage/ast.py @@ -87,7 +87,7 @@ def print(self, printer: Printer): self.value.print(printer) with printer.color(Fore.LIGHTBLUE_EX): printer.write("[") - self.slice.write(printer) + self.slice.print(printer) with printer.color(Fore.LIGHTBLUE_EX): printer.write("]") diff --git a/graphtage/pydiff.py b/graphtage/pydiff.py index a68313c..06f1eb4 100644 --- a/graphtage/pydiff.py +++ b/graphtage/pydiff.py @@ -433,7 +433,7 @@ def print_Subscript(self, printer: Printer, node: Subscript): printer.write("[") self.print(printer, node.slice) with printer.color(Fore.BLUE): - printer.write("[") + printer.write("]") def diff(from_py_obj, to_py_obj, options: BuildOptions | None = None): From fe66b4e5fa95f51548768c7ebf99dccb3d19ff7e Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Wed, 9 Sep 2026 10:16:37 -0400 Subject: [PATCH 2/2] Fix Subscript rendering in both printing paths Subscript nodes have two printing paths, and each carried its own defect. Subscript.print, the fallback that GraphtageFormatter.print uses when no formatter resolves the node type, called self.slice.write(printer). TreeNode has no write method, so printing any subscript through that path emitted the opening bracket and then raised AttributeError. The surrounding lines already call print on the child nodes, so the call now matches them. PyDiffFormatter.print_Subscript wrote an opening bracket where the closing one belongs, rendering a[1] as a[1[. Because the formatter shadows the node's own print method whenever PyDiffFormatter is in play, the two defects never surfaced together, so the regression tests reach each path deliberately: one calls the node's print method directly, the other goes through the formatter. Closes #154 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GypKU5KdLfs2Cf8kS2TzJa --- graphtage/ast.py | 2 +- graphtage/pydiff.py | 2 +- test/test_pydiff.py | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/graphtage/ast.py b/graphtage/ast.py index aeabb13..6c87f58 100644 --- a/graphtage/ast.py +++ b/graphtage/ast.py @@ -87,7 +87,7 @@ def print(self, printer: Printer): self.value.print(printer) with printer.color(Fore.LIGHTBLUE_EX): printer.write("[") - self.slice.write(printer) + self.slice.print(printer) with printer.color(Fore.LIGHTBLUE_EX): printer.write("]") diff --git a/graphtage/pydiff.py b/graphtage/pydiff.py index a68313c..06f1eb4 100644 --- a/graphtage/pydiff.py +++ b/graphtage/pydiff.py @@ -433,7 +433,7 @@ def print_Subscript(self, printer: Printer, node: Subscript): printer.write("[") self.print(printer, node.slice) with printer.color(Fore.BLUE): - printer.write("[") + printer.write("]") def diff(from_py_obj, to_py_obj, options: BuildOptions | None = None): diff --git a/test/test_pydiff.py b/test/test_pydiff.py index ed70fd8..cf03911 100644 --- a/test/test_pydiff.py +++ b/test/test_pydiff.py @@ -1,8 +1,11 @@ import ast import dataclasses +from io import StringIO from unittest import TestCase import graphtage +from graphtage.ast import Subscript +from graphtage.printer import Printer from graphtage.pydiff import PyDiffFormatter, ast_to_tree, build_tree, print_diff from .timing import run_with_time_limit @@ -47,6 +50,29 @@ def test_nested_tuple_diff(self): self.assertIsInstance(kvp.key, graphtage.StringNode) self.assertIsInstance(kvp.value, graphtage.ListNode) + def _only_subscript(self, source: str) -> Subscript: + subscripts = [node for node in ast_to_tree(ast.parse(source)).dfs() if isinstance(node, Subscript)] + self.assertEqual(1, len(subscripts)) + return subscripts[0] + + def test_subscript_node_print(self): + """Reproduces the ``TreeNode.write`` half of https://github.com/trailofbits/graphtage/issues/154 + + ``Subscript.print`` is the fallback that :meth:`graphtage.tree.GraphtageFormatter.print` uses when no + formatter resolves the node type, so this calls it directly rather than through a formatter. + + """ + stream = StringIO() + self._only_subscript("a[1]").print(Printer(out_stream=stream, ansi_color=False)) + self.assertEqual("a[1]", stream.getvalue()) + + def test_subscript_formatter_print(self): + """Reproduces the unbalanced bracket half of https://github.com/trailofbits/graphtage/issues/154""" + stream = StringIO() + node = self._only_subscript("a[1]") + PyDiffFormatter.DEFAULT_INSTANCE.print(Printer(out_stream=stream, ansi_color=False), node) + self.assertEqual("a[1]", stream.getvalue()) + def test_infinite_loop(self): """Reproduces https://github.com/trailofbits/graphtage/issues/82"""