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

Uploaded CPython 3.9+Windows x86-64

sigil_diff-0.6.2-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.2-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.2-cp39-abi3-macosx_11_0_arm64.whl (2.8 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

sigil_diff-0.6.2-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.2.tar.gz.

File metadata

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

File hashes

Hashes for sigil_diff-0.6.2.tar.gz
Algorithm Hash digest
SHA256 5efe3149d51a13ae612fb535a25a8f5b24db98b21b412f80d833e3430e5bcb0a
MD5 fb98a179ca50a3b3fce21a0496661df4
BLAKE2b-256 57a45e54948c15cb424ca0601e5086023bdda1367f049b34f299ec0221da9953

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.6.2-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 2a7bfa8a808cd19616b880b754b622d3e7ad0cdb9293d01e5ad6006343db1738
MD5 8236ffacd77848fbc48a41f03d45b476
BLAKE2b-256 940ea851617a97fe35ff999101e82d94315fe02af79af8d65be15589027b2d04

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.6.2-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 3f166e0c414ee301c88e92d67e44edd00991b2016e45d86600f0441d989e4b6f
MD5 f41ac4eb49ecb5ccf68955367c3cba88
BLAKE2b-256 fe398c46338a204d997b8211baf823a7751db73f63939826241b40e1de6821e5

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.6.2-cp39-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 891997facb341fec6cacc35b351c4982ec6b507786bcae3f3c4a0f4428a173a5
MD5 505da490e20d4a3e557ba496ad22a3ba
BLAKE2b-256 104d898b92d7e9e529e47842beef1ed3912e884d8b583a7050fe3f2ee1cc8899

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.6.2-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 bb26de9743a7a43eca0de18641042bac9847be7a283375b3d8bb3074e8af9756
MD5 701bc40f6e21a8cc19f2965346563625
BLAKE2b-256 8abf452209e1133602981c170d433910a583132b131a523f4ace46f7287918eb

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.6.2-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 faa1d13d9b919aeb75aec12d7702c7fadfa1a382dbbf423105ffca385a97b180
MD5 d2ed4f2ed138969bb86b16c8c0ad2db7
BLAKE2b-256 17c515beaa96c094c91a97bf0fdab8041a7bf666bc47f90e8b91cc6608d6765a

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