unidiff

repository·main·Indexed 43 days ago

https://github.com/matiasb/python-unidiff

A lightweight Python library for parsing unified diff data and extracting metadata. It provides a hierarchical object model consisting of PatchSet, PatchedFile, Hunk, and Line to programmatically inspect file changes, line ranges, and Git-specific modes such as symlinks and submodules. Includes a command-line utility for summarizing diff files and a metadata_only mode for high-performance parsing.

Tokens
4K
Snippets
17
Records
18
Agent score
51%

What's inside unidiff

  1. How PatchSet, PatchedFile, and Hunks work together

    main

    The library follows a hierarchical model for representing unified diff data:

    1. PatchSet: The top-level container representing the entire diff. It acts like a list of PatchedFile objects. You can instantiate it with a file-like object, an iterable of strings, or bytes.
    2. PatchedFile: Represents a single file modified in the patch. It contains metadata (like path, is_added_file, is_modified_file, added count, removed count) and a list of Hunk objects.
    3. Hunk: Represents a specific block of changes within a PatchedFile. It contains line range information (source_start, target_start) and a list of Line objects.
    4. Line: Represents an individual line within a hunk. You can check if a line is part of the addition (is_added) or removal (is_removed) and access its content (value) and line number (source_line_no or target_line_no).

    At any level, calling print() or getting the string representation of these objects returns the unified diff data for that specific scope.

    from unidiff import PatchSet
    
    # Example of the hierarchy
    patch = PatchSet.from_string('--- a/f\n+++ b/f\n@@ -1,2 +1,2 @@\n hola\n-mundo\n+world\n')
    file = patch[0]
    hunk = file[0]
    line = hunk[0]
  2. Access core unidiff components: PatchSet, PatchedFile, and Hunk

    main

    The unidiff library follows a hierarchical structure for representing diffs:

    1. PatchSet: The top-level container representing the entire diff.
    2. PatchedFile: Represents an individual file modified within the diff.
    3. Hunk: Represents a specific block of changes within a PatchedFile.

    These classes are available directly via the unidiff package.

  3. Parse diffs containing embedded carriage returns

    main

    If your diff contains embedded carriage returns (\r) or other control characters, standard Python universal newline mode might incorrectly split lines. To prevent this, open the file with newline='\n' or pass it to from_filename to ensure lines are split only on \n.

    from unidiff import PatchSet
    
    # Use newline='\n' to prevent incorrect line splitting
    patch = PatchSet.from_filename('path/to/diff_with_cr.diff', newline='\n')
  4. Handle Git-specific file modes, symlinks, and submodules

    main

    When parsing Git diffs, unidiff provides helpers to identify special file types based on Git modes:

    • Symlinks: Use is_symlink (detects mode 120000).
    • Submodules: Use is_submodule (detects mode 160000).
    • File Modes: Access source_mode and target_mode for the raw Git mode strings (e.g., '100644', '100755').
    from unidiff import PatchSet
    
    patch = PatchSet.from_filename('git_diff.diff')
    for file in patch:
        if file.is_symlink:
            print(f"{file.path} is a symlink")
        if file.is_submodule:
            print(f"{file.path} is a submodule")
        print(f"Mode: {file.target_mode}")
  5. Inspect files, hunks, and lines in a patch

    main

    Once a PatchSet is loaded, you can traverse its structure to inspect changes.

    PatchedFile attributes:

    • path: The file path.
    • is_modified_file: Boolean indicating if the file was modified.
    • is_added_file: Boolean indicating if the file is new.
    • added: Number of lines added.
    • removed: Number of lines removed.
    • is_binary_file: Boolean indicating if the file is binary.
    • diff_line_no: The 1-based line number in the diff where this file's entry starts.

    Hunk attributes:

    • source_start: Starting line number in the source file.
    • target_start: Starting line number in the target file.

    Line attributes:

    • is_added: True if the line is an addition.
    • is_removed: True if the line is a removal.
    • value: The content of the line.
    • source_line_no: The line number in the source file.
    • target_line_no: The line number in the target file.
    from unidiff import PatchSet
    
    patch = PatchSet.from_string('--- a/f\n+++ b/f\n@@ -1,2 +1,2 @@\n hola\n-mundo\n+world\n')
    file = patch[0]
    hunk = file[0]
    
    # Inspecting a line
    for line in hunk:
        if line.is_removed:
            print(f"Removed: {line.value} at line {line.source_line_no}")
        elif line.is_added:
            print(f"Added: {line.value} at line {line.target_line_no}")
  6. Load a diff from a file-like object or iterable

    main

    You can instantiate PatchSet by passing a file-like object or an iterable (such as a list of lines).

    from unidiff import PatchSet
    
    # Using a file-like object
    with open('diff.diff', 'r', encoding='utf-8') as f:
        patch = PatchSet(f)
    
    # Using an iterable of lines
    with open('diff.diff', 'r') as f:
        lines = f.readlines()
        patch = PatchSet(lines)
  7. Load a diff using PatchSet.from_filename

    main

    Use the PatchSet.from_filename class method to read a diff directly from a local file. You can optionally specify an encoding and set metadata_only=True to improve parsing efficiency if you do not need to rebuild the original diff string.

    from unidiff import PatchSet
    
    # Standard usage
    patch = PatchSet.from_filename('path/to/diff.diff', encoding='utf-8')
    
    # Efficient parsing (metadata only)
    patch = PatchSet.from_filename('path/to/diff.diff', encoding='utf-8', metadata_only=True)
  8. Use the unidiff CLI tool

    main

    The unidiff package provides a command-line utility that summarizes diff data from a file or stdin.

    # Summarize a git diff via pipe
    $ git diff | unidiff
    
    # Output format example:
    # Summary
    # -------
    # README.md: +6 additions, -0 deletions
    # 
    # 1 modified file(s), 0 added file(s), 0 removed file(s)
    # Total: 6 addition(s), 0 deletion(s)
  9. Analyze a PatchedFile

    main

    A PatchedFile represents a single file modified within a diff. It contains metadata and a list of Hunk objects.

    File Metadata:

    • path: The abstracted file path (handles VCS prefixes and quotes).
    • is_added_file: True if the file is new.
    • is_removed_file: True if the file was deleted.
    • is_modified_file: True if the file was changed.
    • is_rename: True if the file was renamed.
    • is_binary_file: True if the file is a binary diff.
    • is_symlink: True if the file is a symbolic link.
    • is_submodule: True if the file is a git submodule.
    • source_file / target_file: The original and new filenames.
    • added / removed: Total lines added/removed in this specific file.

    File Content:

    • added / removed: Total lines added/removed in this specific file.
    for file in patch.modified_files:
        print(f"File: {file.path}")
        print(f"  Added: {file.added}")
        print(f"  Removed: {file.removed}")
        if file.is_symlink:
            print("  Note: This is a symlink")
  10. Parse diffs using PatchSet

    main

    The primary entry point for the unidiff library is the PatchSet class. You use it to parse a unified diff format into a structured object model containing files, hunks, and lines. If the diff format is invalid, it will raise a UnidiffParseError.

    from unidiff import PatchSet
    
    # Assuming 'diff_content' is a string or file-like object containing a unified diff
    patch_set = PatchSet(diff_content)
    for patched_file in patch_set:
        print(f"File: {patched_file.path}")
  11. Parse a diff with PatchSet

    main

    The PatchSet class is the primary entry point for parsing unified diff data. It represents a collection of PatchedFile objects. You can initialize it from a file, a string, or raw bytes.

    Key features:

    • from_filename(filename, ...): Loads a diff from a file on disk.
    • from_string(data, ...): Loads a diff from a string or bytes.
    • metadata_only=True: When enabled, the parser only processes hunk headers and metadata, skipping line content. This is approximately 2.5x to 6x faster and is useful for quickly calculating line counts or file changes without inspecting the actual content.
    from unidiff import PatchSet
    
    # From a file
    patch = PatchSet.from_filename('changes.diff')
    
    # From a string
    diff_text = """--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-old\n+new"""
    patch = PatchSet.from_string(diff_text)
    
    # Fast metadata-only parsing
    fast_patch = PatchSet.from_string(diff_text, metadata_only=True)