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.0.tar.gz (1.5 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.0-cp39-abi3-win_amd64.whl (2.5 MB view details)

Uploaded CPython 3.9+Windows x86-64

sigil_diff-0.5.0-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.5.0-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.0-cp39-abi3-macosx_11_0_arm64.whl (2.7 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

sigil_diff-0.5.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.5.0.tar.gz.

File metadata

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

File hashes

Hashes for sigil_diff-0.5.0.tar.gz
Algorithm Hash digest
SHA256 57617ecda9205673486d1ec4888666d9d71a3754876b708240db6221ffd1487f
MD5 5a269fb4384d7b5cb31e03fcc5840141
BLAKE2b-256 0ac8d809b7fbcb018c2643c9ea901ce27eb149e166499d08674ca107e80d7ea7

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 694f958c9699f69eecce76f27d7bb1c406a04d743c5c64edd789c629fe0eaee1
MD5 b75774080caea92a497a9d8058229af0
BLAKE2b-256 99be42f799d4a35c273f590d83744363dc3338f4dc6598de31d2865cb1098889

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.0-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 daeb9f7443e5aaa5e7b326af7f839f48770a122e75e37083d7f589e825af6f6e
MD5 536447c38d031ea63e2640eb5670b1a8
BLAKE2b-256 4315dc7c489a8a4c84b934777d7e3f22c2720b09e894f942c39df89e6e62d923

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.0-cp39-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 37001d55163081f498667ee053df97ea329b2520e88362dd7613402676d1f986
MD5 3da0eb94b727b9ad02eaf6ac01fc19bf
BLAKE2b-256 5fffa1c4c7bedb21afbc22dce2d3dd7d30e420c240e371d71f86c77f51f810aa

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 330fa4858def4e639ba59822073173de58d975fb630c5e7e9afd1475c088199c
MD5 9cf77b2429d9ba4897fefc2e1e0f4596
BLAKE2b-256 07f2a760e4e75d7b1a74d237ebc2e808b20c0241f8c35517feb76ff67ae7f66c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for sigil_diff-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 b8f3598d9b4fadcf0adc9b05f1871ddcfd9aa285ff27c63603e8246864bda5ce
MD5 d9a643bf2722d54325e41d9cd0f325d4
BLAKE2b-256 a95b8a668b913e2d4110a00ca93e0087540d79706b2d531a997a5eb1041f3fc8

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