trailofbits / trailofbits/graphtage

Document how to consume a diff programmatically

Open Beginner friendly
#185 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Python
Stars
2.5k
Forks
61
Avg merge
6h 45m
Merged PRs (30d)
45

Description

docs/library.rst shows how to build trees, diff them, and print the result, but it stops before the case where you want to keep the diff instead of printing it. Splitting this out of #75, where the reporter wanted to feed two document trees to Graphtage and get back a third tree whose changed subtrees are wrapped in their own Diff(before, after) node, and had to reverse-engineer the API to do it.

Everything needed is already public. graphtage.pydiff.diff returns an edited tree, and every node in it is a graphtage.EditedTreeNode carrying removed, inserted, matched_to, edit, and edit_list (graphtage/tree.py:283-323). Those attributes are populated by Match.on_diff, Remove.on_diff, and Insert.on_diff (graphtage/edits.py:310, :373, :406). What is missing is a page that puts them together:

from graphtage.pydiff import diff

def summarize(node, depth=0):
    for child in node.children():
        if child.removed:
            print("  " * depth, "removed:", child.to_obj())
        elif child.matched_to is not None and child.edit.has_non_zero_cost():
            print("  " * depth, "changed:", child.to_obj(), "->", child.matched_to.to_obj())
        for inserted in child.inserted:
            print("  " * depth, "inserted:", inserted.to_obj())
        summarize(child, depth + 1)

summarize(diff({"a": 1, "b": [1, 3]}, {"a": 2, "b": [1, 2, 3]}))
 changed: 1 -> 2
 inserted: 2

Points that are not obvious from the current documentation and that the page should state:

  • Insert.on_diff annotates the container, not the inserted node, so you read insertions from a parent's inserted list rather than from a flag on the new child.
  • A changed scalar comes back as a Match with a non-zero cost, not as a Replace, so testing isinstance(edit, Replace) misses most value changes. Use has_non_zero_cost().
  • TreeNode.get_all_edit_contexts (graphtage/tree.py:558) yields (ancestor_path, edit) pairs and is the better starting point when you need to know where an edit happened. __main__.py:389 uses it to implement --edit-digest.
  • to_obj() converts a node back to plain Python, but its docstring currently scopes it to --match-if expression evaluation. Either widen that or point readers at TreeNode.copy() and copy_from() instead.

Suggested home: a new section in docs/library.rst after "Diffing In-Memory Python Objects".

Filed to track the part of #75 that remains after the UTF-8 decoding error reported there was fixed in v0.2.8. cc @SamWilsn

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the "Diffing In-Memory Python Objects" section in docs/library.rst, then read graphtage.pydiff.diff and the EditedTreeNode attributes in graphtage/tree.py. Review TreeNode.get_all_edit_contexts and its use in main.py, and use the supplied example as the check. Done means the new section explains collecting changed, removed, and inserted nodes, edit contexts, and converting nodes to plain Python.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation
Issue type
Documentation
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
90/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.