Skip to main content

Structural code fingerprinting and diffing — Python bindings

Project description

sigil-diff

Python bindings for sigil — structural code fingerprinting and diffing.

pip install sigil-diff
import sigil

Powered by Rust via PyO3. Native speed, no subprocess overhead, works with in-memory strings.

Quick Start

import sigil

old = '{"body": {"text": "Hello world"}, "header": {"text": ""}}'
new = '{"body": {"text": "Hello universe"}, "header": {"text": ""}}'

result = sigil.diff_json(old, new)

for f in result["files"]:
    for entity in f["entities"]:
        print(f"{entity['change']}: {entity['name']}")
        for tc in entity.get("token_changes", []):
            print(f"  \"{tc['from']}\" -> \"{tc['to']}\"")

Output:

modified: body.text
  ""text": "Hello world"," -> ""text": "Hello universe","

API

sigil.diff_json(old: str, new: str) -> dict

Diff two JSON strings. Returns a structured dict with all changes.

This is the primary function for comparing JSON documents (notification templates, config files, API responses, etc.). Minified JSON is automatically formatted before diffing.

result = sigil.diff_json(old_json_str, new_json_str)

Features applied automatically:

  • Parent-aware matching (no cross-matching between body.text and header.text)
  • Derived field suppression (_-prefixed fields hidden from output)
  • Array item identity matching (objects matched by id/key/name/text/type)
  • Qualified names (body.text instead of bare text)
  • Parent entity suppression (when children carry the detail)

sigil.diff_files(old_path: str, new_path: str) -> dict

Diff two files on disk. Supports JSON, YAML, TOML, Markdown, Python, Rust, Go, JavaScript, TypeScript, Java, Ruby, C, C++, C#, Swift, Kotlin.

result = sigil.diff_files("templates/old.json", "templates/new.json")

sigil.diff_refs(repo_path: str, base_ref: str, head_ref: str) -> dict

Diff two git refs in a repository. Works with any ref format: commit SHAs, branch names, tags, HEAD~N.

result = sigil.diff_refs("/path/to/repo", "main", "feature-branch")
result = sigil.diff_refs(".", "HEAD~1", "HEAD")

sigil.index_json(source: str) -> list[dict]

Parse a JSON string into structural entities. Returns a list of entity dicts.

entities = sigil.index_json('{"name": "test", "tags": ["a", "b"], "_internal": "hidden"}')

for e in entities:
    derived = " (derived)" if e.get("meta") and "derived" in e["meta"] else ""
    parent = f" -> {e['parent']}" if e.get("parent") else ""
    print(f"{e['name']}: {e['kind']}{parent}{derived}")

Output:

name: property
tags: array
[0]: element -> tags
[1]: element -> tags
_internal: property (derived)

Return Format

All diff functions return a dict with this structure:

{
    "meta": {
        "base_ref": "old",
        "head_ref": "new",
        "sigil_version": "0.2.4"
    },
    "summary": {
        "files_changed": 1,
        "added": 0,
        "removed": 0,
        "modified": 1,
        "renamed": 0,
        "has_breaking": False,
        "natural_language": "1 modified."
    },
    "files": [
        {
            "file": "doc.json",
            "summary": {"added": 0, "modified": 1, "removed": 0, ...},
            "entities": [
                {
                    "change": "modified",       # added | removed | modified | renamed | formatting_only
                    "name": "body.text",         # qualified name for JSON entities
                    "kind": "property",          # property | object | array | element | function | class | ...
                    "line": 3,
                    "line_end": 3,
                    "sig_changed": False,        # signature (key name/type) changed?
                    "body_changed": True,         # value changed?
                    "breaking": False,
                    "token_changes": [
                        {
                            "type": "value_changed",
                            "from": "Hello world",
                            "to": "Hello universe"
                        }
                    ],
                    "context": {                  # source snippets for display
                        "hunks": [
                            {"kind": "removed", "text": "..."},
                            {"kind": "added", "text": "..."}
                        ]
                    }
                }
            ]
        }
    ],
    "breaking": [],       # list of breaking change entries
    "patterns": [],       # cross-file patterns (e.g., same rename across files)
    "moves": []           # entities moved between files
}

Examples

Compare notification templates

import sigil
import json

old_template = json.dumps({
    "body": {
        "text": "Hi {{user.name}}, your order #{{order_id}} is confirmed.",
        "_parsed_text": "Hi {{1}}, your order #{{2}} is confirmed.",
        "_examples": ["John", "12345"]
    },
    "buttons": [
        {"text": "Track Order", "type": "URL"}
    ]
})

new_template = json.dumps({
    "body": {
        "text": "Hi {{user.name}}, order #{{order_id}} is on its way!",
        "_parsed_text": "Hi {{1}}, order #{{2}} is on its way!",
        "_examples": ["John", "12345"]
    },
    "buttons": [
        {"text": "Track Order", "type": "URL"}
    ]
})

result = sigil.diff_json(old_template, new_template)

if result["summary"]["modified"] > 0:
    for f in result["files"]:
        for e in f["entities"]:
            print(f"Changed: {e['name']}")
            for tc in e.get("token_changes", []):
                print(f"  {tc['from']}")
                print(f"  {tc['to']}")

Check if two documents are identical

result = sigil.diff_json(old_str, new_str)
is_identical = result["summary"]["modified"] == 0 and result["summary"]["added"] == 0 and result["summary"]["removed"] == 0

Get changed field names

result = sigil.diff_json(old_str, new_str)
changed_fields = [
    e["name"]
    for f in result["files"]
    for e in f["entities"]
]

Development

# Clone the repo
git clone https://github.com/knova-run/sigil.git
cd sigil/python

# Create venv and build
python3 -m venv .venv
source .venv/bin/activate
pip install maturin
maturin develop

# Test
python3 -c "import sigil; print(sigil.diff_json('{\"a\":1}', '{\"a\":2}'))"

Build a wheel

maturin build --release
# Output: target/wheels/sigil_diff-0.2.4-cp3XX-*.whl

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sigil_diff-0.6.0.tar.gz (2.2 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

sigil_diff-0.6.0-cp39-abi3-win_amd64.whl (2.6 MB view details)

Uploaded CPython 3.9+Windows x86-64

sigil_diff-0.6.0-cp39-abi3-manylinux_2_28_x86_64.whl (3.0 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ x86-64

sigil_diff-0.6.0-cp39-abi3-manylinux_2_28_aarch64.whl (2.8 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ ARM64

sigil_diff-0.6.0-cp39-abi3-macosx_11_0_arm64.whl (2.8 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

sigil_diff-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl (2.7 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file sigil_diff-0.6.0.tar.gz.

File metadata

  • Download URL: sigil_diff-0.6.0.tar.gz
  • Upload date:
  • Size: 2.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.13.3

File hashes

Hashes for sigil_diff-0.6.0.tar.gz
Algorithm Hash digest
SHA256 fa67ffffb054d53c18c239ca5bc067d167cbce6edbc371372b2248fbb6659200
MD5 e08c34bdfaa602cd20024bde265b3fc9
BLAKE2b-256 6d3f234036eebdf3c0c47a0e5754c8f67a41010b16c1bb6b072084bca6762bb6

See more details on using hashes here.

File details

Details for the file sigil_diff-0.6.0-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for sigil_diff-0.6.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 bc16a289d174e573b98e01b36736a6777c46efb53dab7c17a92dc18d202053e8
MD5 09cb4535af3ea35bc427313bcb6d9690
BLAKE2b-256 8ba727214bd517ee78a5826817b919b7724e25cf2b339c95abff058a5bee7a31

See more details on using hashes here.

File details

Details for the file sigil_diff-0.6.0-cp39-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for sigil_diff-0.6.0-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 045a9e56e7d1e2fd29a251d4b8ed3f20907491f5771bd7b8e4efd38be1990a52
MD5 6c78caa5852c7c286441a9f8a4fb9a30
BLAKE2b-256 c0b27842088055bda7e2000322ecdfd1751238013724dff5c4483601c61cc6ff

See more details on using hashes here.

File details

Details for the file sigil_diff-0.6.0-cp39-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for sigil_diff-0.6.0-cp39-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 c35ffadde5c9380a09af7954095c3bf3c637a82c4dab11c6034c1000dec33b17
MD5 cec2e2c8f82a314960a79a859eaf2462
BLAKE2b-256 03307fd3d6aa8f944ff7afc724f4de53fd9593e8e76265632447b57245a130e3

See more details on using hashes here.

File details

Details for the file sigil_diff-0.6.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for sigil_diff-0.6.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9694b467d4fdceb3b8f883df23c20f6097bd090ef3f0659902fd69cbb4ae5100
MD5 1ee1f1143a505f320132e58231a3e90e
BLAKE2b-256 c32f4cb5483995dea46553cf66eb87f0b05466e1632e7026cd95d3e736c1a9ce

See more details on using hashes here.

File details

Details for the file sigil_diff-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for sigil_diff-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 a7a19fce0d23f2f6eba558854b2e77cc395e3ae5ba791d1962040197f1e51770
MD5 c7fe5192ade8b80df32715510e112f29
BLAKE2b-256 55715d96ec70fbaa809d83e17b927e8f428ee65f0811b29503021bf2596e0584

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page