Add support for a new file format in 30 minutes.
This guide walks you through creating a polydiff plugin for any file format. By the end, you'll have a working plugin that integrates seamlessly with polydiff and git.
- Overview
- Quick Start
- Plugin Interface
- Implementation Steps
- Registration
- Testing
- Publishing
- Examples
- Best Practices
polydiff uses a plugin architecture based on Python entry points. Plugins are:
- Self-contained: Each plugin is a Python class that implements the
DiffPlugininterface - Discoverable: Plugins are found automatically via entry points in
pyproject.toml - Independent: Plugins can be distributed as separate packages
Create a new file in src/polydiff/plugins/ (or in your own package):
# src/polydiff/plugins/cad_diff.py
from pathlib import Path
from polydiff.core.plugin_base import DiffPlugin, DiffResult, DiffOptions
class CadDiffPlugin(DiffPlugin):
"""Plugin for diffing CAD files."""
name = "polydiff-cad"
supported_extensions = [".dwg", ".dxf"]
def diff(self, path_a: Path, path_b: Path, options: DiffOptions) -> DiffResult:
"""Compare two CAD files."""
# Your implementation here
similarity = 0.95 # Compute this
changed = similarity < 1.0
return DiffResult(
similarity=similarity,
changed=changed,
summary=f"{similarity:.1%} similar",
terminal_output=self._build_terminal_output(similarity, changed),
json_output={
"similarity": similarity,
"changed": changed,
},
)
def _build_terminal_output(self, similarity: float, changed: bool) -> str:
"""Build Rich-formatted terminal output."""
if not changed:
return "[green]✓ Files are identical[/green]"
return f"[yellow]~ {similarity:.1%} similar[/yellow]"Add to your pyproject.toml:
[project.entry-points."polydiff/plugins"]
cad = "polydiff.plugins.cad_diff:CadDiffPlugin"# Install your plugin
pip install -e .
# Verify it's discovered
polydiff plugins list
# Test it
polydiff diff old.dxf new.dxfAll plugins must inherit from DiffPlugin and implement:
class DiffPlugin(ABC):
"""Base class every format plugin must implement."""
name: str # e.g. "polydiff-cad"
supported_extensions: list[str] # e.g. [".dwg", ".dxf"]
@abstractmethod
def diff(self, path_a: Path, path_b: Path, options: DiffOptions) -> DiffResult:
"""Compare two files of the same format and return a DiffResult."""
...
def supports(self, path: Path) -> bool:
"""Check if this plugin supports the given file."""
return path.suffix.lower() in self.supported_extensionsControls diff behavior:
@dataclass
class DiffOptions:
output_format: str = "terminal" # "terminal" | "html" | "json" | "image"
output_path: Optional[Path] = None # Where to save output (for html/image)
context_lines: int = 3 # For text diffs
color: bool = True # Enable colored outputThe result of a diff operation:
@dataclass
class DiffResult:
similarity: float # 0.0 (different) to 1.0 (identical)
changed: bool # True if similarity < 1.0
summary: str # One-line human-readable summary
terminal_output: str # Rich-markup-formatted string
html_output: Optional[str] = None # HTML fragment for reports
json_output: dict = field(default_factory=dict) # Structured data
artifact_path: Optional[Path] = None # Path to generated artifactBefore coding, understand:
- What can change? (visual, structural, metadata, content)
- What libraries exist? (check PyPI, GitHub)
- What's a useful diff? (summary, detailed, visual)
def diff(self, path_a: Path, path_b: Path, options: DiffOptions) -> DiffResult:
# 1. Load both files
try:
data_a = self._load(path_a)
data_b = self._load(path_b)
except Exception as e:
return DiffResult(
similarity=0.0,
changed=True,
summary=f"Error: {e}",
terminal_output=f"[red]Error:[/red] {e}",
)
# 2. Compare them
similarity = self._compute_similarity(data_a, data_b)
changes = self._find_changes(data_a, data_b)
# 3. Build output
changed = similarity < 1.0
return DiffResult(
similarity=similarity,
changed=changed,
summary=self._build_summary(changes),
terminal_output=self._build_terminal(changes),
html_output=self._build_html(changes) if options.output_format == "html" else None,
json_output=self._build_json(changes),
)Use Rich markup for colored output:
def _build_terminal(self, changes: list) -> str:
lines = []
if not changes:
lines.append("[green]✓ Files are identical[/green]")
else:
lines.append(f"[yellow]{len(changes)} change(s) found[/yellow]")
for change in changes[:10]: # Limit output
lines.append(f" • {change}")
return "\n".join(lines)Return an HTML fragment (not a full page):
def _build_html(self, changes: list) -> str:
html = ["<h2>Diff Report</h2>"]
if changes:
html.append("<ul>")
for change in changes:
html.append(f"<li>{change}</li>")
html.append("</ul>")
else:
html.append("<p>No changes found.</p>")
return "\n".join(html)Provide structured data for programmatic use:
def _build_json(self, changes: list) -> dict:
return {
"changes_count": len(changes),
"changes": changes,
}Add to polydiff's pyproject.toml:
[project.entry-points."polydiff/plugins"]
your_format = "polydiff.plugins.your_diff:YourDiffPlugin"Create a new package:
polydiff-plugin-cad/
├── pyproject.toml
├── src/
│ └── polydiff_plugin_cad/
│ ├── __init__.py
│ └── cad_diff.py
└── tests/
In pyproject.toml:
[project]
name = "polydiff-plugin-cad"
version = "0.1.0"
dependencies = [
"polydiff>=0.1.0",
"ezdxf>=0.18", # Example CAD library
]
[project.entry-points."polydiff/plugins"]
cad = "polydiff_plugin_cad.cad_diff:CadDiffPlugin"# tests/test_cad_diff.py
from pathlib import Path
import pytest
from polydiff.core.plugin_base import DiffOptions
from polydiff.plugins.cad_diff import CadDiffPlugin
FIXTURES_DIR = Path(__file__).parent / "fixtures" / "cad"
@pytest.fixture
def plugin():
return CadDiffPlugin()
@pytest.fixture
def options():
return DiffOptions(output_format="terminal", color=False)
class TestCadDiffPlugin:
def test_identical_files(self, plugin, options):
result = plugin.diff(
FIXTURES_DIR / "identical_a.dxf",
FIXTURES_DIR / "identical_b.dxf",
options,
)
assert result.similarity == pytest.approx(1.0, abs=0.01)
assert result.changed is False
def test_changed_files(self, plugin, options):
result = plugin.diff(
FIXTURES_DIR / "original.dxf",
FIXTURES_DIR / "modified.dxf",
options,
)
assert result.changed is True
assert result.similarity < 1.0
def test_supports_extension(self, plugin):
assert plugin.supports(Path("test.dxf"))
assert plugin.supports(Path("test.dwg"))
assert not plugin.supports(Path("test.txt"))Create test files in tests/fixtures/:
tests/fixtures/
└── cad/
├── identical_a.dxf
├── identical_b.dxf
├── original.dxf
└── modified.dxf
# Update version
# pyproject.toml: version = "0.1.0"
# Add README
# README.md
# Add LICENSE
# LICENSE (MIT recommended)# Install build tools
pip install build twine
# Build
python -m build
# Check
twine check dist/*
# Upload to PyPI
twine upload dist/*- Open a PR to add your plugin to polydiff's README
- Share on relevant communities
See src/polydiff/plugins/image_diff.py for a complete example including:
- SSIM computation
- Pixel diff visualization
- HTML output with embedded images
See src/polydiff/plugins/pdf_diff.py for:
- Page-by-page comparison
- Text extraction
- Visual and text diff combination
See src/polydiff/plugins/xlsx_diff.py for:
- Cell-level diffing
- Formula comparison
- Sheet-level changes
def diff(self, path_a, path_b, options):
try:
data_a = self._load(path_a)
except Exception as e:
return DiffResult(
similarity=0.0,
changed=True,
summary=f"Error loading {path_a.name}: {e}",
terminal_output=f"[red]Error:[/red] {e}",
)# Bad
summary = "Files differ"
# Good
summary = "3 layers changed, 2 objects added, 1 object removed"# Don't dump 10,000 changes to terminal
for change in changes[:50]:
lines.append(f" • {change}")
if len(changes) > 50:
lines.append(f" [dim]... and {len(changes) - 50} more[/dim]")# 1.0 = identical
# 0.99+ = trivial differences (whitespace, timestamps)
# 0.9-0.99 = minor changes
# 0.5-0.9 = significant changes
# 0.0-0.5 = major changes or completely differentAlways implement terminal_output. Consider implementing:
html_outputfor rich reportsjson_outputfor programmatic useartifact_pathfor visual diffs
- Open an issue on GitHub
- Check existing plugins for examples
- Read the source code (it's well-documented!)
Plugins should use the MIT license to match polydiff's license.
Happy plugin writing! 🎉
Your plugin helps make polydiff more useful for everyone. Thank you for contributing!