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/gauravverma/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.3.1.tar.gz (621.6 kB 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.3.1-cp39-abi3-win_amd64.whl (2.5 MB view details)

Uploaded CPython 3.9+Windows x86-64

sigil_diff-0.3.1-cp39-abi3-manylinux_2_28_x86_64.whl (2.8 MB view details)

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

sigil_diff-0.3.1-cp39-abi3-manylinux_2_28_aarch64.whl (2.7 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ ARM64

sigil_diff-0.3.1-cp39-abi3-macosx_11_0_arm64.whl (2.7 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

sigil_diff-0.3.1-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.3.1.tar.gz.

File metadata

  • Download URL: sigil_diff-0.3.1.tar.gz
  • Upload date:
  • Size: 621.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.13.1

File hashes

Hashes for sigil_diff-0.3.1.tar.gz
Algorithm Hash digest
SHA256 850afbb3f7fd06af7fa6facf292a2e01fc45d168a95efae544285303f07944a9
MD5 f173e71660b9a1a0b5ffbdd07dfc93d7
BLAKE2b-256 8322dac411848479ef2b617c23efaf8f0f915b30c0aca79c9d597642a1cb611d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.3.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 9e77d6ad7f5cc7ff69b06128ae82343539168054eb695fde4684ba373355e51f
MD5 2804a41a3638f4989fcb0a6e7ad27631
BLAKE2b-256 1e2431133a25e7bac9052abcc54add149b7946909c722fba97804db8a65367dc

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.3.1-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 2911b83b856bf83b498d7f04799bdaa11cc6a5d7018416ccceae89e0bb16cc35
MD5 40f6a128d075d97e2d5c8918a7b66fad
BLAKE2b-256 d7b007b37ea4314715af0b085cc2ca49430f5bda9ec7fc5db3571aa09bb8721c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.3.1-cp39-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 a51ab07b95d7ea9f5748ef00cb71bf3a3b23f1a54bd15d8f08e089d51830c775
MD5 026a506f8684f01a49c77a763f9ca96c
BLAKE2b-256 ef2c2bf16cebbf2185c31a3f4605d412bff2133321d91011bfd7b8b6419d3fbd

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.3.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 11c20bc2a7233943d4ccc0e73d6b37c5514da491a02b4e49875c95a78c3e9244
MD5 490cd1610f920c0f783a54ab167d0aca
BLAKE2b-256 0d4336934c67d967d17f231a5ad821cb52efb48fc103b52f4bfe48f586385f2a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.3.1-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 9e6383a0c5951db2954a8cda58cb8fe457ac8e90be3e527f3f9bcc351edbe5fd
MD5 8c22be15e1fd8bc6a7b46e96bf21c128
BLAKE2b-256 48a05cd34dbcc7dd3e56a97e5b871979c5917f44afff4ecc4240f24508e27083

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