Extract Python API signatures and detect breaking changes for documentation generation.
—
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.
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))
"""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
"""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.
"""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.
"""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.
"""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.
"""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"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()}")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)}")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)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}")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'])}")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