Skip to content

Diff

lcp.diff

Diff module for comparing two LCP documents and detecting deprecated symbols.

SymbolDiff dataclass

Description of a single symbol difference between two LCP documents.

Source code in src/lcp/diff.py
@dataclass
class SymbolDiff:
    """Description of a single symbol difference between two LCP documents."""

    symbol_id: str
    kind: str
    module: str | None = None
    summary: str | None = None

DiffResult dataclass

Result of comparing two LCP documents.

Attributes:

Name Type Description
old_version str

Version string of the old library.

new_version str

Version string of the new library.

library_name str

Name of the library.

removed list[SymbolDiff]

Symbols present in old but absent in new (potential deprecations).

added list[SymbolDiff]

Symbols present in new but absent in old.

deprecated dict[str, Deprecation]

Deprecation entries for removed symbols, keyed by symbol ID.

Source code in src/lcp/diff.py
@dataclass
class DiffResult:
    """Result of comparing two LCP documents.

    Attributes:
        old_version: Version string of the old library.
        new_version: Version string of the new library.
        library_name: Name of the library.
        removed: Symbols present in old but absent in new (potential deprecations).
        added: Symbols present in new but absent in old.
        deprecated: Deprecation entries for removed symbols, keyed by symbol ID.
    """

    old_version: str
    new_version: str
    library_name: str
    removed: list[SymbolDiff] = field(default_factory=list)
    added: list[SymbolDiff] = field(default_factory=list)
    deprecated: dict[str, Deprecation] = field(default_factory=dict)

    def to_dict(self) -> dict:
        """Convert to a JSON-serializable dictionary."""
        return {
            "library": self.library_name,
            "old_version": self.old_version,
            "new_version": self.new_version,
            "summary": {
                "removed": len(self.removed),
                "added": len(self.added),
            },
            "removed": [
                {
                    "symbol_id": s.symbol_id,
                    "kind": s.kind,
                    "module": s.module,
                    "summary": s.summary,
                }
                for s in self.removed
            ],
            "added": [
                {
                    "symbol_id": s.symbol_id,
                    "kind": s.kind,
                    "module": s.module,
                    "summary": s.summary,
                }
                for s in self.added
            ],
            "deprecations": {
                sid: dep.model_dump(mode="json", exclude_none=True)
                for sid, dep in self.deprecated.items()
            },
        }

    def to_json(self, indent: int = 2) -> str:
        """Convert to JSON string."""
        return json.dumps(self.to_dict(), indent=indent)

to_dict

to_dict() -> dict

Convert to a JSON-serializable dictionary.

Source code in src/lcp/diff.py
def to_dict(self) -> dict:
    """Convert to a JSON-serializable dictionary."""
    return {
        "library": self.library_name,
        "old_version": self.old_version,
        "new_version": self.new_version,
        "summary": {
            "removed": len(self.removed),
            "added": len(self.added),
        },
        "removed": [
            {
                "symbol_id": s.symbol_id,
                "kind": s.kind,
                "module": s.module,
                "summary": s.summary,
            }
            for s in self.removed
        ],
        "added": [
            {
                "symbol_id": s.symbol_id,
                "kind": s.kind,
                "module": s.module,
                "summary": s.summary,
            }
            for s in self.added
        ],
        "deprecations": {
            sid: dep.model_dump(mode="json", exclude_none=True)
            for sid, dep in self.deprecated.items()
        },
    }

to_json

to_json(indent: int = 2) -> str

Convert to JSON string.

Source code in src/lcp/diff.py
def to_json(self, indent: int = 2) -> str:
    """Convert to JSON string."""
    return json.dumps(self.to_dict(), indent=indent)

diff_documents

diff_documents(old: LCPDocument, new: LCPDocument) -> DiffResult

Compare two LCP documents and detect differences.

Symbols present in old but missing from new are treated as removed (deprecated). For each removed symbol a Deprecation entry is created with deprecated_in set to the new document's version.

Parameters:

Name Type Description Default
old LCPDocument

The earlier LCP document.

required
new LCPDocument

The later LCP document.

required

Returns:

Type Description
DiffResult

A DiffResult containing removed and added symbols together with

DiffResult

generated deprecation entries.

Source code in src/lcp/diff.py
def diff_documents(old: LCPDocument, new: LCPDocument) -> DiffResult:
    """Compare two LCP documents and detect differences.

    Symbols present in *old* but missing from *new* are treated as removed
    (deprecated).  For each removed symbol a ``Deprecation`` entry is
    created with ``deprecated_in`` set to the *new* document's version.

    Args:
        old: The earlier LCP document.
        new: The later LCP document.

    Returns:
        A DiffResult containing removed and added symbols together with
        generated deprecation entries.
    """
    old_ids = set(old.symbols.keys())
    new_ids = set(new.symbols.keys())

    removed_ids = sorted(old_ids - new_ids)
    added_ids = sorted(new_ids - old_ids)

    new_version = new.manifest.library.version
    library_name = new.manifest.library.name

    removed = [_symbol_diff_from(sid, old.symbols[sid]) for sid in removed_ids]
    added = [_symbol_diff_from(sid, new.symbols[sid]) for sid in added_ids]

    # Build deprecation entries for removed symbols
    deprecated: dict[str, Deprecation] = {}
    for sid in removed_ids:
        deprecated[sid] = Deprecation(deprecated_in=new_version)

    return DiffResult(
        old_version=old.manifest.library.version,
        new_version=new_version,
        library_name=library_name,
        removed=removed,
        added=added,
        deprecated=deprecated,
    )

update_document

update_document(document: LCPDocument, diff_result: DiffResult) -> LCPDocument

Return a copy of document with deprecation entries from diff_result merged in.

Existing deprecation entries in the document are preserved. New entries from the diff result are added for symbols that were removed between versions.

Parameters:

Name Type Description Default
document LCPDocument

The LCP document to update (typically the new version).

required
diff_result DiffResult

The result of :func:diff_documents.

required

Returns:

Type Description
LCPDocument

A new LCPDocument with merged deprecation entries.

Source code in src/lcp/diff.py
def update_document(document: LCPDocument, diff_result: DiffResult) -> LCPDocument:
    """Return a copy of *document* with deprecation entries from *diff_result* merged in.

    Existing deprecation entries in the document are preserved.  New entries
    from the diff result are added for symbols that were removed between
    versions.

    Args:
        document: The LCP document to update (typically the *new* version).
        diff_result: The result of :func:`diff_documents`.

    Returns:
        A new LCPDocument with merged deprecation entries.
    """
    existing = dict(document.deprecations) if document.deprecations else {}
    merged = {**existing, **{
        sid: dep for sid, dep in diff_result.deprecated.items()
        if sid not in existing
    }}

    return document.model_copy(update={
        "deprecations": merged if merged else None,
    })

load_lcp_document

load_lcp_document(path: str) -> LCPDocument

Load an LCP document from a JSON file.

Parameters:

Name Type Description Default
path str

File path to an LCP JSON file.

required

Returns:

Type Description
LCPDocument

A parsed LCPDocument.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

JSONDecodeError

If the file is not valid JSON.

ValidationError

If the JSON does not match the LCP schema.

Source code in src/lcp/diff.py
def load_lcp_document(path: str) -> LCPDocument:
    """Load an LCP document from a JSON file.

    Args:
        path: File path to an LCP JSON file.

    Returns:
        A parsed LCPDocument.

    Raises:
        FileNotFoundError: If the file does not exist.
        json.JSONDecodeError: If the file is not valid JSON.
        pydantic.ValidationError: If the JSON does not match the LCP schema.
    """
    with open(path, encoding="utf-8") as f:
        data = json.load(f)
    return LCPDocument.model_validate(data)