0
# Breaking Changes
1
2
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.
3
4
## Capabilities
5
6
### Breakage Detection
7
8
Main function for finding breaking changes between two API versions.
9
10
```python { .api }
11
def find_breaking_changes(
12
old_object: Object,
13
new_object: Object,
14
**kwargs: Any,
15
) -> Iterator[Breakage]:
16
"""
17
Find breaking changes between two versions of the same API.
18
19
Compares two Griffe objects (typically modules or packages) and identifies
20
changes that could break backwards compatibility for consumers of the API.
21
22
Args:
23
old_object: The previous version of the API
24
new_object: The current version of the API
25
**kwargs: Additional comparison options
26
27
Yields:
28
Breakage: Individual breaking changes found
29
30
Examples:
31
Compare Git versions:
32
>>> old_api = griffe.load_git("mypackage", ref="v1.0.0")
33
>>> new_api = griffe.load("mypackage")
34
>>> breakages = list(griffe.find_breaking_changes(old_api, new_api))
35
36
Compare PyPI versions:
37
>>> old_pypi = griffe.load_pypi("requests", "2.28.0")
38
>>> new_pypi = griffe.load_pypi("requests", "2.29.0")
39
>>> breakages = list(griffe.find_breaking_changes(old_pypi, new_pypi))
40
"""
41
```
42
43
### Base Breakage Class
44
45
Abstract base class for all API breakage types.
46
47
```python { .api }
48
class Breakage:
49
"""
50
Base class for API breakages.
51
52
Represents changes that could break backwards compatibility.
53
All specific breakage types inherit from this class.
54
"""
55
56
def __init__(
57
self,
58
object_path: str,
59
old_value: Any = None,
60
new_value: Any = None,
61
**kwargs: Any,
62
) -> None:
63
"""
64
Initialize the breakage.
65
66
Args:
67
object_path: Dotted path to the affected object
68
old_value: Previous value (if applicable)
69
new_value: New value (if applicable)
70
**kwargs: Additional breakage-specific data
71
"""
72
73
@property
74
def kind(self) -> BreakageKind:
75
"""The type/kind of this breakage."""
76
77
@property
78
def object_path(self) -> str:
79
"""Dotted path to the affected object."""
80
81
@property
82
def old_value(self) -> Any:
83
"""Previous value before the change."""
84
85
@property
86
def new_value(self) -> Any:
87
"""New value after the change."""
88
89
def explain(self) -> str:
90
"""
91
Get a human-readable explanation of the breakage.
92
93
Returns:
94
str: Description of what changed and why it's breaking
95
"""
96
```
97
98
## Object-Level Breakages
99
100
Breaking changes related to entire objects (removal, kind changes).
101
102
```python { .api }
103
class ObjectRemovedBreakage(Breakage):
104
"""
105
A public object was removed from the API.
106
107
This breakage occurs when a previously public class, function,
108
method, or attribute is no longer available.
109
"""
110
111
class ObjectChangedKindBreakage(Breakage):
112
"""
113
An object's kind changed (e.g., function became a class).
114
115
This breakage occurs when an object changes its fundamental type,
116
such as a function becoming a class or vice versa.
117
"""
118
```
119
120
## Parameter-Related Breakages
121
122
Breaking changes in function/method signatures.
123
124
```python { .api }
125
class ParameterAddedRequiredBreakage(Breakage):
126
"""
127
A required parameter was added to a function signature.
128
129
This breakage occurs when a new parameter without a default value
130
is added to a function, making existing calls invalid.
131
"""
132
133
class ParameterRemovedBreakage(Breakage):
134
"""
135
A parameter was removed from a function signature.
136
137
This breakage occurs when a parameter is removed entirely,
138
breaking code that passed that parameter.
139
"""
140
141
class ParameterChangedDefaultBreakage(Breakage):
142
"""
143
A parameter's default value changed.
144
145
This breakage occurs when the default value of a parameter changes,
146
potentially altering behavior for code that relies on the default.
147
"""
148
149
class ParameterChangedKindBreakage(Breakage):
150
"""
151
A parameter's kind changed (e.g., positional to keyword-only).
152
153
This breakage occurs when parameter calling conventions change,
154
such as making a positional parameter keyword-only.
155
"""
156
157
class ParameterChangedRequiredBreakage(Breakage):
158
"""
159
A parameter became required (lost its default value).
160
161
This breakage occurs when a parameter that previously had a default
162
value no longer has one, making it required.
163
"""
164
165
class ParameterMovedBreakage(Breakage):
166
"""
167
A parameter changed position in the function signature.
168
169
This breakage occurs when parameters are reordered, potentially
170
breaking positional argument usage.
171
"""
172
```
173
174
## Type-Related Breakages
175
176
Breaking changes in type annotations and return types.
177
178
```python { .api }
179
class ReturnChangedTypeBreakage(Breakage):
180
"""
181
A function's return type annotation changed.
182
183
This breakage occurs when the return type annotation of a function
184
changes in a way that could break type checking or expectations.
185
"""
186
187
class AttributeChangedTypeBreakage(Breakage):
188
"""
189
An attribute's type annotation changed.
190
191
This breakage occurs when an attribute's type annotation changes
192
in an incompatible way.
193
"""
194
195
class AttributeChangedValueBreakage(Breakage):
196
"""
197
An attribute's value changed.
198
199
This breakage occurs when the value of a constant or class attribute
200
changes, potentially breaking code that depends on the specific value.
201
"""
202
```
203
204
## Class-Related Breakages
205
206
Breaking changes in class hierarchies and inheritance.
207
208
```python { .api }
209
class ClassRemovedBaseBreakage(Breakage):
210
"""
211
A base class was removed from a class's inheritance.
212
213
This breakage occurs when a class no longer inherits from a base class,
214
potentially breaking isinstance() checks and inherited functionality.
215
"""
216
```
217
218
## Breakage Classification
219
220
```python { .api }
221
from enum import Enum
222
223
class BreakageKind(Enum):
224
"""
225
Enumeration of possible API breakage types.
226
227
Used to classify different kinds of breaking changes for
228
filtering and reporting purposes.
229
"""
230
231
# Object-level changes
232
OBJECT_REMOVED = "object_removed"
233
OBJECT_CHANGED_KIND = "object_changed_kind"
234
235
# Parameter changes
236
PARAMETER_ADDED_REQUIRED = "parameter_added_required"
237
PARAMETER_REMOVED = "parameter_removed"
238
PARAMETER_CHANGED_DEFAULT = "parameter_changed_default"
239
PARAMETER_CHANGED_KIND = "parameter_changed_kind"
240
PARAMETER_CHANGED_REQUIRED = "parameter_changed_required"
241
PARAMETER_MOVED = "parameter_moved"
242
243
# Type changes
244
RETURN_CHANGED_TYPE = "return_changed_type"
245
ATTRIBUTE_CHANGED_TYPE = "attribute_changed_type"
246
ATTRIBUTE_CHANGED_VALUE = "attribute_changed_value"
247
248
# Class hierarchy changes
249
CLASS_REMOVED_BASE = "class_removed_base"
250
```
251
252
## Usage Examples
253
254
### Basic Breaking Change Detection
255
256
```python
257
import griffe
258
259
# Compare two versions
260
old_version = griffe.load_git("mypackage", ref="v1.0.0")
261
new_version = griffe.load("mypackage")
262
263
# Find all breaking changes
264
breakages = list(griffe.find_breaking_changes(old_version, new_version))
265
266
print(f"Found {len(breakages)} breaking changes:")
267
for breakage in breakages:
268
print(f" {breakage.kind.value}: {breakage.object_path}")
269
print(f" {breakage.explain()}")
270
```
271
272
### Filtering Breaking Changes
273
274
```python
275
import griffe
276
from griffe import BreakageKind
277
278
# Get breaking changes
279
breakages = list(griffe.find_breaking_changes(old_api, new_api))
280
281
# Filter by type
282
removed_objects = [
283
b for b in breakages
284
if b.kind == BreakageKind.OBJECT_REMOVED
285
]
286
287
parameter_changes = [
288
b for b in breakages
289
if b.kind.value.startswith("parameter_")
290
]
291
292
type_changes = [
293
b for b in breakages
294
if "type" in b.kind.value
295
]
296
297
print(f"Removed objects: {len(removed_objects)}")
298
print(f"Parameter changes: {len(parameter_changes)}")
299
print(f"Type changes: {len(type_changes)}")
300
```
301
302
### CI/CD Integration
303
304
```python
305
import sys
306
import griffe
307
308
def check_api_compatibility(old_ref: str, package_name: str) -> int:
309
"""Check API compatibility and return exit code for CI."""
310
try:
311
# Load versions
312
old_api = griffe.load_git(package_name, ref=old_ref)
313
new_api = griffe.load(package_name)
314
315
# Find breaking changes
316
breakages = list(griffe.find_breaking_changes(old_api, new_api))
317
318
if breakages:
319
print(f"❌ Found {len(breakages)} breaking changes:")
320
for breakage in breakages:
321
print(f" • {breakage.explain()}")
322
return 1
323
else:
324
print("✅ No breaking changes detected")
325
return 0
326
327
except Exception as e:
328
print(f"❌ Error checking compatibility: {e}")
329
return 2
330
331
# Use in CI script
332
exit_code = check_api_compatibility("v1.0.0", "mypackage")
333
sys.exit(exit_code)
334
```
335
336
### Detailed Breakage Analysis
337
338
```python
339
import griffe
340
341
# Compare versions with detailed analysis
342
old_api = griffe.load_pypi("requests", "2.28.0")
343
new_api = griffe.load_pypi("requests", "2.29.0")
344
345
breakages = list(griffe.find_breaking_changes(old_api, new_api))
346
347
# Group by breakage type
348
by_type = {}
349
for breakage in breakages:
350
kind = breakage.kind.value
351
if kind not in by_type:
352
by_type[kind] = []
353
by_type[kind].append(breakage)
354
355
# Report by category
356
for breakage_type, items in by_type.items():
357
print(f"\n{breakage_type.replace('_', ' ').title()} ({len(items)}):")
358
for item in items:
359
print(f" • {item.object_path}")
360
if hasattr(item, 'old_value') and item.old_value is not None:
361
print(f" Old: {item.old_value}")
362
if hasattr(item, 'new_value') and item.new_value is not None:
363
print(f" New: {item.new_value}")
364
```
365
366
### Custom Breakage Analysis
367
368
```python
369
import griffe
370
from griffe import Breakage, BreakageKind
371
372
class CustomBreakageAnalyzer:
373
"""Custom analyzer for breaking changes."""
374
375
def __init__(self, ignore_patterns: list[str] = None):
376
self.ignore_patterns = ignore_patterns or []
377
378
def analyze(self, old_api, new_api) -> dict:
379
"""Analyze breaking changes with custom logic."""
380
breakages = list(griffe.find_breaking_changes(old_api, new_api))
381
382
# Filter ignored patterns
383
filtered_breakages = []
384
for breakage in breakages:
385
if not any(pattern in breakage.object_path for pattern in self.ignore_patterns):
386
filtered_breakages.append(breakage)
387
388
# Categorize by severity
389
critical = []
390
moderate = []
391
minor = []
392
393
for breakage in filtered_breakages:
394
if breakage.kind in [BreakageKind.OBJECT_REMOVED, BreakageKind.PARAMETER_ADDED_REQUIRED]:
395
critical.append(breakage)
396
elif breakage.kind in [BreakageKind.PARAMETER_CHANGED_KIND, BreakageKind.RETURN_CHANGED_TYPE]:
397
moderate.append(breakage)
398
else:
399
minor.append(breakage)
400
401
return {
402
"critical": critical,
403
"moderate": moderate,
404
"minor": minor,
405
"total": len(filtered_breakages)
406
}
407
408
# Use custom analyzer
409
analyzer = CustomBreakageAnalyzer(ignore_patterns=["_internal", "test_"])
410
results = analyzer.analyze(old_api, new_api)
411
412
print(f"Critical: {len(results['critical'])}")
413
print(f"Moderate: {len(results['moderate'])}")
414
print(f"Minor: {len(results['minor'])}")
415
```
416
417
## Types
418
419
```python { .api }
420
from typing import Any, Iterator
421
from enum import Enum
422
423
# Core types from models
424
from griffe import Object
425
426
# Breakage enumeration
427
class BreakageKind(Enum):
428
"""Enumeration of breakage types."""
429
430
# Base breakage type
431
class Breakage:
432
"""Base breakage class."""
433
```