CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-griffe

Extract Python API signatures and detect breaking changes for documentation generation.

Pending
Overview
Eval results
Files

breaking-changes.mddocs/

Breaking Changes

Comprehensive API breakage detection system for identifying changes that could break backwards compatibility between different versions of Python packages. This system enables automated API compatibility checking in CI/CD pipelines and release workflows.

Capabilities

Breakage Detection

Main function for finding breaking changes between two API versions.

def find_breaking_changes(
    old_object: Object,
    new_object: Object,
    **kwargs: Any,
) -> Iterator[Breakage]:
    """
    Find breaking changes between two versions of the same API.
    
    Compares two Griffe objects (typically modules or packages) and identifies
    changes that could break backwards compatibility for consumers of the API.
    
    Args:
        old_object: The previous version of the API
        new_object: The current version of the API  
        **kwargs: Additional comparison options
        
    Yields:
        Breakage: Individual breaking changes found
        
    Examples:
        Compare Git versions:
        >>> old_api = griffe.load_git("mypackage", ref="v1.0.0")
        >>> new_api = griffe.load("mypackage")
        >>> breakages = list(griffe.find_breaking_changes(old_api, new_api))
        
        Compare PyPI versions:
        >>> old_pypi = griffe.load_pypi("requests", "2.28.0")
        >>> new_pypi = griffe.load_pypi("requests", "2.29.0")
        >>> breakages = list(griffe.find_breaking_changes(old_pypi, new_pypi))
    """

Base Breakage Class

Abstract base class for all API breakage types.

class Breakage:
    """
    Base class for API breakages.
    
    Represents changes that could break backwards compatibility.
    All specific breakage types inherit from this class.
    """
    
    def __init__(
        self,
        object_path: str,
        old_value: Any = None,
        new_value: Any = None,
        **kwargs: Any,
    ) -> None:
        """
        Initialize the breakage.
        
        Args:
            object_path: Dotted path to the affected object
            old_value: Previous value (if applicable)
            new_value: New value (if applicable)
            **kwargs: Additional breakage-specific data
        """
    
    @property
    def kind(self) -> BreakageKind:
        """The type/kind of this breakage."""
    
    @property
    def object_path(self) -> str:
        """Dotted path to the affected object."""
    
    @property
    def old_value(self) -> Any:
        """Previous value before the change."""
    
    @property
    def new_value(self) -> Any:
        """New value after the change."""
    
    def explain(self) -> str:
        """
        Get a human-readable explanation of the breakage.
        
        Returns:
            str: Description of what changed and why it's breaking
        """

Object-Level Breakages

Breaking changes related to entire objects (removal, kind changes).

class ObjectRemovedBreakage(Breakage):
    """
    A public object was removed from the API.
    
    This breakage occurs when a previously public class, function,
    method, or attribute is no longer available.
    """

class ObjectChangedKindBreakage(Breakage):
    """
    An object's kind changed (e.g., function became a class).
    
    This breakage occurs when an object changes its fundamental type,
    such as a function becoming a class or vice versa.
    """

Parameter-Related Breakages

Breaking changes in function/method signatures.

class ParameterAddedRequiredBreakage(Breakage):
    """
    A required parameter was added to a function signature.
    
    This breakage occurs when a new parameter without a default value
    is added to a function, making existing calls invalid.
    """

class ParameterRemovedBreakage(Breakage):
    """
    A parameter was removed from a function signature.
    
    This breakage occurs when a parameter is removed entirely,
    breaking code that passed that parameter.
    """

class ParameterChangedDefaultBreakage(Breakage):
    """
    A parameter's default value changed.
    
    This breakage occurs when the default value of a parameter changes,
    potentially altering behavior for code that relies on the default.
    """

class ParameterChangedKindBreakage(Breakage):
    """
    A parameter's kind changed (e.g., positional to keyword-only).
    
    This breakage occurs when parameter calling conventions change,
    such as making a positional parameter keyword-only.
    """

class ParameterChangedRequiredBreakage(Breakage):
    """
    A parameter became required (lost its default value).
    
    This breakage occurs when a parameter that previously had a default
    value no longer has one, making it required.
    """

class ParameterMovedBreakage(Breakage):
    """
    A parameter changed position in the function signature.
    
    This breakage occurs when parameters are reordered, potentially
    breaking positional argument usage.
    """

Type-Related Breakages

Breaking changes in type annotations and return types.

class ReturnChangedTypeBreakage(Breakage):
    """
    A function's return type annotation changed.
    
    This breakage occurs when the return type annotation of a function
    changes in a way that could break type checking or expectations.
    """

class AttributeChangedTypeBreakage(Breakage):
    """
    An attribute's type annotation changed.
    
    This breakage occurs when an attribute's type annotation changes
    in an incompatible way.
    """

class AttributeChangedValueBreakage(Breakage):
    """
    An attribute's value changed.
    
    This breakage occurs when the value of a constant or class attribute
    changes, potentially breaking code that depends on the specific value.
    """

Class-Related Breakages

Breaking changes in class hierarchies and inheritance.

class ClassRemovedBaseBreakage(Breakage):
    """
    A base class was removed from a class's inheritance.
    
    This breakage occurs when a class no longer inherits from a base class,
    potentially breaking isinstance() checks and inherited functionality.
    """

Breakage Classification

from enum import Enum

class BreakageKind(Enum):
    """
    Enumeration of possible API breakage types.
    
    Used to classify different kinds of breaking changes for
    filtering and reporting purposes.
    """
    
    # Object-level changes
    OBJECT_REMOVED = "object_removed"
    OBJECT_CHANGED_KIND = "object_changed_kind"
    
    # Parameter changes  
    PARAMETER_ADDED_REQUIRED = "parameter_added_required"
    PARAMETER_REMOVED = "parameter_removed"
    PARAMETER_CHANGED_DEFAULT = "parameter_changed_default"
    PARAMETER_CHANGED_KIND = "parameter_changed_kind"
    PARAMETER_CHANGED_REQUIRED = "parameter_changed_required"
    PARAMETER_MOVED = "parameter_moved"
    
    # Type changes
    RETURN_CHANGED_TYPE = "return_changed_type"
    ATTRIBUTE_CHANGED_TYPE = "attribute_changed_type"
    ATTRIBUTE_CHANGED_VALUE = "attribute_changed_value"
    
    # Class hierarchy changes
    CLASS_REMOVED_BASE = "class_removed_base"

Usage Examples

Basic Breaking Change Detection

import griffe

# Compare two versions
old_version = griffe.load_git("mypackage", ref="v1.0.0")
new_version = griffe.load("mypackage")

# Find all breaking changes
breakages = list(griffe.find_breaking_changes(old_version, new_version))

print(f"Found {len(breakages)} breaking changes:")
for breakage in breakages:
    print(f"  {breakage.kind.value}: {breakage.object_path}")
    print(f"    {breakage.explain()}")

Filtering Breaking Changes

import griffe
from griffe import BreakageKind

# Get breaking changes
breakages = list(griffe.find_breaking_changes(old_api, new_api))

# Filter by type
removed_objects = [
    b for b in breakages 
    if b.kind == BreakageKind.OBJECT_REMOVED
]

parameter_changes = [
    b for b in breakages
    if b.kind.value.startswith("parameter_")
]

type_changes = [
    b for b in breakages
    if "type" in b.kind.value
]

print(f"Removed objects: {len(removed_objects)}")
print(f"Parameter changes: {len(parameter_changes)}")  
print(f"Type changes: {len(type_changes)}")

CI/CD Integration

import sys
import griffe

def check_api_compatibility(old_ref: str, package_name: str) -> int:
    """Check API compatibility and return exit code for CI."""
    try:
        # Load versions
        old_api = griffe.load_git(package_name, ref=old_ref)
        new_api = griffe.load(package_name)
        
        # Find breaking changes
        breakages = list(griffe.find_breaking_changes(old_api, new_api))
        
        if breakages:
            print(f"❌ Found {len(breakages)} breaking changes:")
            for breakage in breakages:
                print(f"  • {breakage.explain()}")
            return 1
        else:
            print("✅ No breaking changes detected")
            return 0
            
    except Exception as e:
        print(f"❌ Error checking compatibility: {e}")
        return 2

# Use in CI script
exit_code = check_api_compatibility("v1.0.0", "mypackage")
sys.exit(exit_code)

Detailed Breakage Analysis

import griffe

# Compare versions with detailed analysis
old_api = griffe.load_pypi("requests", "2.28.0")
new_api = griffe.load_pypi("requests", "2.29.0")

breakages = list(griffe.find_breaking_changes(old_api, new_api))

# Group by breakage type
by_type = {}
for breakage in breakages:
    kind = breakage.kind.value
    if kind not in by_type:
        by_type[kind] = []
    by_type[kind].append(breakage)

# Report by category
for breakage_type, items in by_type.items():
    print(f"\n{breakage_type.replace('_', ' ').title()} ({len(items)}):")
    for item in items:
        print(f"  • {item.object_path}")
        if hasattr(item, 'old_value') and item.old_value is not None:
            print(f"    Old: {item.old_value}")
        if hasattr(item, 'new_value') and item.new_value is not None:
            print(f"    New: {item.new_value}")

Custom Breakage Analysis

import griffe
from griffe import Breakage, BreakageKind

class CustomBreakageAnalyzer:
    """Custom analyzer for breaking changes."""
    
    def __init__(self, ignore_patterns: list[str] = None):
        self.ignore_patterns = ignore_patterns or []
    
    def analyze(self, old_api, new_api) -> dict:
        """Analyze breaking changes with custom logic."""
        breakages = list(griffe.find_breaking_changes(old_api, new_api))
        
        # Filter ignored patterns  
        filtered_breakages = []
        for breakage in breakages:
            if not any(pattern in breakage.object_path for pattern in self.ignore_patterns):
                filtered_breakages.append(breakage)
        
        # Categorize by severity
        critical = []
        moderate = []
        minor = []
        
        for breakage in filtered_breakages:
            if breakage.kind in [BreakageKind.OBJECT_REMOVED, BreakageKind.PARAMETER_ADDED_REQUIRED]:
                critical.append(breakage)
            elif breakage.kind in [BreakageKind.PARAMETER_CHANGED_KIND, BreakageKind.RETURN_CHANGED_TYPE]:
                moderate.append(breakage)
            else:
                minor.append(breakage)
        
        return {
            "critical": critical,
            "moderate": moderate, 
            "minor": minor,
            "total": len(filtered_breakages)
        }

# Use custom analyzer
analyzer = CustomBreakageAnalyzer(ignore_patterns=["_internal", "test_"])
results = analyzer.analyze(old_api, new_api)

print(f"Critical: {len(results['critical'])}")
print(f"Moderate: {len(results['moderate'])}")
print(f"Minor: {len(results['minor'])}")

Types

from typing import Any, Iterator
from enum import Enum

# Core types from models
from griffe import Object

# Breakage enumeration
class BreakageKind(Enum):
    """Enumeration of breakage types."""
    
# Base breakage type
class Breakage:
    """Base breakage class."""

Install with Tessl CLI

npx tessl i tessl/pypi-griffe

docs

agents.md

breaking-changes.md

cli.md

docstrings.md

extensions.md

index.md

loaders.md

models.md

serialization.md

tile.json