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.5.5.tar.gz (2.0 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.5.5-cp39-abi3-win_amd64.whl (2.6 MB view details)

Uploaded CPython 3.9+Windows x86-64

sigil_diff-0.5.5-cp39-abi3-manylinux_2_28_x86_64.whl (2.9 MB view details)

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

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

Uploaded CPython 3.9+manylinux: glibc 2.28+ ARM64

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

Uploaded CPython 3.9+macOS 11.0+ ARM64

sigil_diff-0.5.5-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.5.5.tar.gz.

File metadata

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

File hashes

Hashes for sigil_diff-0.5.5.tar.gz
Algorithm Hash digest
SHA256 de35cf520d88aad10f3d5131ee410c1bf1e29f7fe8a594e1b7b7d44ad3012448
MD5 eeec14d2da1ff42ef438d06d96ff2bc5
BLAKE2b-256 5c921534fdc08d207a2952c2eee64d2f8bdb4632679dd578d9a049157e467c1e

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.5-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 9b54ab6318130cb8a28c864f26045350b8a9bccc2215ca2c41eae2f900a1236d
MD5 5924ac56cb44b5d021a9f97fc815d890
BLAKE2b-256 d8503eb868850abb53d1e9d74ead8caaf101684f1904fbdafda510471d1e882a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.5-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 e02a6cba8e640dcbecbdfdf46ff1927d8c80a552179068d79e3f14b4aa0a524c
MD5 81a46c51f13f124b17ae5606c28662f0
BLAKE2b-256 763dfd343d59f7ec3cfcd6bb0a7345c941dcb6287f1b2da0eafbf7984c3da3c8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.5-cp39-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 8cb017b9dfd04c46283fa2cdee03e7f3877991ccd3564aec263222a2dfcdabd9
MD5 0b43e1b4db65a9a9ebf648887eea1662
BLAKE2b-256 bef9488d979e585e8831d2eb0e83b1d55192d5d793695419ca5d78af87c21572

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.5-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 da6800ea15d7d5eacc754073add51cf08bb0d27a3326b596fd982dea7299a12f
MD5 c6b7969018eddf33137d2ab77dc753f6
BLAKE2b-256 0335ad6008d0ab412cbfe26a4b65cf898bd0d270cb734a221b20ebfe23119cf4

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.5-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 6c5a53d21a88d6fcaa6bb5c42f0ccfb4edc61fa9c7fc9a613c07f52bdade5a13
MD5 b9a56814908543abe3359516ceabc65e
BLAKE2b-256 8e085cbd05afc20657b503bf5d5cfbd2e069ae04660ce97bd0d94501c696b5a5

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