0
# Extensions
1
2
Extensible plugin system for customizing and enhancing Griffe's analysis capabilities. Extensions allow developers to add custom processing during code analysis, handle special decorators, implement domain-specific analysis, and extend Griffe's understanding of Python constructs.
3
4
## Capabilities
5
6
### Extension Loading
7
8
Functions for loading and managing extensions from various sources.
9
10
```python { .api }
11
def load_extensions(
12
extensions: list[LoadableExtensionType] | None = None
13
) -> Extensions:
14
"""
15
Load configured extensions from various sources.
16
17
Supports loading built-in extensions, installed packages, and custom
18
extension classes. Extensions are executed during different phases
19
of the analysis process.
20
21
Args:
22
extensions: List of extension specifications including:
23
- String names for built-in extensions ("dataclasses")
24
- Import paths for installed extensions ("package.module:ExtensionClass")
25
- Extension class instances
26
- Extension class types
27
28
Returns:
29
Extensions: Container with loaded and configured extensions
30
31
Raises:
32
ExtensionNotLoadedError: If extension cannot be loaded
33
34
Examples:
35
Load built-in extensions:
36
>>> extensions = griffe.load_extensions(["dataclasses"])
37
38
Load custom extensions:
39
>>> extensions = griffe.load_extensions([
40
... "dataclasses", # built-in
41
... "mypackage.extensions:CustomAnalyzer", # installed
42
... MyExtension(), # instance
43
... ])
44
"""
45
46
# Type alias for extension specifications
47
LoadableExtensionType = (
48
str | # Extension name or import path
49
type[Extension] | # Extension class
50
Extension | # Extension instance
51
dict[str, Any] # Extension configuration
52
)
53
54
# Built-in extensions registry
55
builtin_extensions: dict[str, type[Extension]]
56
"""Dictionary of built-in extensions available in Griffe."""
57
```
58
59
### Base Extension Class
60
61
Foundation class for creating custom extensions with lifecycle hooks.
62
63
```python { .api }
64
class Extension:
65
"""
66
Base class for Griffe extensions.
67
68
Allows custom processing during code analysis by providing hooks
69
at different stages of the loading and analysis process.
70
"""
71
72
def __init__(self, **options: Any) -> None:
73
"""
74
Initialize the extension.
75
76
Args:
77
**options: Extension-specific configuration options
78
"""
79
80
def on_package_loaded(self, *, pkg: Module) -> None:
81
"""
82
Hook called when a package is loaded.
83
84
Called after a top-level package (module) has been fully loaded
85
and all its contents analyzed. Use this for package-level processing.
86
87
Args:
88
pkg: The loaded package module
89
"""
90
91
def on_module_loaded(self, *, mod: Module) -> None:
92
"""
93
Hook called when a module is loaded.
94
95
Called after a module has been loaded and its contents analyzed.
96
Includes both top-level packages and submodules.
97
98
Args:
99
mod: The loaded module
100
"""
101
102
def on_class_loaded(self, *, cls: Class) -> None:
103
"""
104
Hook called when a class is loaded.
105
106
Called after a class definition has been analyzed and all its
107
methods, attributes, and nested classes have been processed.
108
109
Args:
110
cls: The loaded class
111
"""
112
113
def on_function_loaded(self, *, func: Function) -> None:
114
"""
115
Hook called when a function is loaded.
116
117
Called after a function or method has been analyzed, including
118
parameter extraction and signature processing.
119
120
Args:
121
func: The loaded function or method
122
"""
123
124
def on_attribute_loaded(self, *, attr: Attribute) -> None:
125
"""
126
Hook called when an attribute is loaded.
127
128
Called after an attribute (variable, class attribute, etc.)
129
has been analyzed and its type and value determined.
130
131
Args:
132
attr: The loaded attribute
133
"""
134
135
def on_alias_loaded(self, *, alias: Alias) -> None:
136
"""
137
Hook called when an alias is loaded.
138
139
Called after an import alias has been processed and its
140
target path determined.
141
142
Args:
143
alias: The loaded alias
144
"""
145
```
146
147
### Extensions Container
148
149
Container class for managing multiple extensions and their execution.
150
151
```python { .api }
152
class Extensions:
153
"""
154
Container class for managing multiple extensions.
155
156
Manages a collection of extensions and provides methods to execute
157
their hooks at appropriate times during analysis.
158
"""
159
160
def __init__(self, extensions: list[Extension] | None = None) -> None:
161
"""
162
Initialize extensions container.
163
164
Args:
165
extensions: List of extension instances
166
"""
167
168
def add(self, extension: Extension) -> None:
169
"""
170
Add an extension to the container.
171
172
Args:
173
extension: Extension instance to add
174
"""
175
176
def call(self, method: str, **kwargs: Any) -> None:
177
"""
178
Call a method on all extensions.
179
180
Args:
181
method: Method name to call (e.g., "on_class_loaded")
182
**kwargs: Arguments to pass to the method
183
"""
184
185
def __iter__(self) -> Iterator[Extension]:
186
"""Iterate over extensions."""
187
188
def __len__(self) -> int:
189
"""Number of extensions."""
190
```
191
192
## Built-in Extensions
193
194
### Dataclasses Extension
195
196
Built-in extension for enhanced analysis of Python dataclasses.
197
198
```python { .api }
199
class DataclassesExtension(Extension):
200
"""
201
Built-in extension for handling Python dataclasses.
202
203
Provides enhanced analysis of dataclass decorators, field definitions,
204
and generated methods. Automatically detects dataclass usage and
205
extracts field information with types and defaults.
206
"""
207
208
def __init__(self, **options: Any) -> None:
209
"""
210
Initialize dataclasses extension.
211
212
Args:
213
**options: Configuration options for dataclass analysis
214
"""
215
216
def on_class_loaded(self, *, cls: Class) -> None:
217
"""
218
Process dataclass when class is loaded.
219
220
Analyzes @dataclass decorators and extracts field information,
221
adding synthetic attributes for dataclass fields.
222
"""
223
```
224
225
## Usage Examples
226
227
### Using Built-in Extensions
228
229
```python
230
import griffe
231
232
# Load with dataclasses extension
233
extensions = griffe.load_extensions(["dataclasses"])
234
loader = griffe.GriffeLoader(extensions=extensions)
235
236
# Load a package that uses dataclasses
237
package = loader.load("mypackage")
238
239
# The dataclasses extension will have processed any @dataclass decorators
240
for class_name, cls in package.classes.items():
241
if any("dataclass" in str(dec.value) for dec in cls.decorators):
242
print(f"Dataclass: {class_name}")
243
print(f" Fields: {list(cls.attributes.keys())}")
244
```
245
246
### Creating Custom Extensions
247
248
```python
249
import griffe
250
from griffe import Extension, Class, Function
251
252
class LoggingExtension(Extension):
253
"""Extension that logs all loaded objects."""
254
255
def __init__(self, log_level="INFO", **options):
256
super().__init__(**options)
257
self.log_level = log_level
258
self.stats = {"modules": 0, "classes": 0, "functions": 0}
259
260
def on_module_loaded(self, *, mod):
261
self.stats["modules"] += 1
262
print(f"[{self.log_level}] Loaded module: {mod.path}")
263
264
def on_class_loaded(self, *, cls):
265
self.stats["classes"] += 1
266
print(f"[{self.log_level}] Loaded class: {cls.path}")
267
268
# Log inheritance information
269
if cls.bases:
270
base_names = [str(base) for base in cls.bases]
271
print(f" Inherits from: {', '.join(base_names)}")
272
273
def on_function_loaded(self, *, func):
274
self.stats["functions"] += 1
275
print(f"[{self.log_level}] Loaded function: {func.path}")
276
277
# Log parameter information
278
param_count = len(func.parameters)
279
print(f" Parameters: {param_count}")
280
281
# Use the custom extension
282
logging_ext = LoggingExtension(log_level="DEBUG")
283
extensions = griffe.Extensions([logging_ext])
284
285
loader = griffe.GriffeLoader(extensions=extensions)
286
package = loader.load("requests")
287
288
print("Final stats:", logging_ext.stats)
289
```
290
291
### Domain-Specific Analysis Extension
292
293
```python
294
import griffe
295
from griffe import Extension, Function, Class
296
297
class FastAPIExtension(Extension):
298
"""Extension for analyzing FastAPI applications."""
299
300
def __init__(self, **options):
301
super().__init__(**options)
302
self.routes = []
303
self.dependencies = []
304
self.middleware = []
305
306
def on_function_loaded(self, *, func: Function):
307
"""Analyze FastAPI route decorators."""
308
for decorator in func.decorators:
309
decorator_str = str(decorator.value)
310
311
# Check for route decorators
312
if any(method in decorator_str for method in ["get", "post", "put", "delete", "patch"]):
313
route_info = {
314
"function": func.path,
315
"decorator": decorator_str,
316
"parameters": [p.name for p in func.parameters],
317
"returns": str(func.returns) if func.returns else None
318
}
319
self.routes.append(route_info)
320
print(f"Found route: {func.name} -> {decorator_str}")
321
322
# Check for dependency injection
323
elif "Depends" in decorator_str:
324
self.dependencies.append({
325
"function": func.path,
326
"dependency": decorator_str
327
})
328
329
def on_class_loaded(self, *, cls: Class):
330
"""Analyze FastAPI middleware classes."""
331
for decorator in cls.decorators:
332
if "middleware" in str(decorator.value).lower():
333
self.middleware.append({
334
"class": cls.path,
335
"decorator": str(decorator.value)
336
})
337
338
def get_api_summary(self):
339
"""Get summary of FastAPI application structure."""
340
return {
341
"routes": len(self.routes),
342
"dependencies": len(self.dependencies),
343
"middleware": len(self.middleware),
344
"route_details": self.routes
345
}
346
347
# Use FastAPI extension
348
fastapi_ext = FastAPIExtension()
349
extensions = griffe.Extensions([fastapi_ext])
350
351
loader = griffe.GriffeLoader(extensions=extensions)
352
app_module = loader.load("myapp")
353
354
# Get API analysis results
355
summary = fastapi_ext.get_api_summary()
356
print(f"FastAPI Analysis: {summary['routes']} routes, {summary['dependencies']} dependencies")
357
```
358
359
### Type Annotation Extension
360
361
```python
362
import griffe
363
from griffe import Extension, Function, Attribute
364
import ast
365
366
class TypeAnnotationExtension(Extension):
367
"""Extension for enhanced type annotation analysis."""
368
369
def __init__(self, **options):
370
super().__init__(**options)
371
self.type_stats = {
372
"annotated_functions": 0,
373
"unannotated_functions": 0,
374
"annotated_attributes": 0,
375
"complex_types": 0
376
}
377
378
def on_function_loaded(self, *, func: Function):
379
"""Analyze function type annotations."""
380
if func.returns or any(p.annotation for p in func.parameters):
381
self.type_stats["annotated_functions"] += 1
382
383
# Check for complex return types
384
if func.returns:
385
return_str = str(func.returns)
386
if any(keyword in return_str for keyword in ["Union", "Optional", "Generic", "TypeVar"]):
387
self.type_stats["complex_types"] += 1
388
print(f"Complex return type in {func.name}: {return_str}")
389
else:
390
self.type_stats["unannotated_functions"] += 1
391
392
def on_attribute_loaded(self, *, attr: Attribute):
393
"""Analyze attribute type annotations."""
394
if attr.annotation:
395
self.type_stats["annotated_attributes"] += 1
396
397
# Check for complex attribute types
398
attr_str = str(attr.annotation)
399
if any(keyword in attr_str for keyword in ["Union", "Optional", "List", "Dict"]):
400
print(f"Complex attribute type: {attr.path}: {attr_str}")
401
402
def get_type_coverage(self):
403
"""Calculate type annotation coverage."""
404
total_functions = self.type_stats["annotated_functions"] + self.type_stats["unannotated_functions"]
405
if total_functions > 0:
406
coverage = (self.type_stats["annotated_functions"] / total_functions) * 100
407
return {
408
"function_coverage": round(coverage, 2),
409
"annotated_functions": self.type_stats["annotated_functions"],
410
"total_functions": total_functions,
411
"complex_types": self.type_stats["complex_types"],
412
"annotated_attributes": self.type_stats["annotated_attributes"]
413
}
414
return {"function_coverage": 0, "total_functions": 0}
415
416
# Use type annotation extension
417
type_ext = TypeAnnotationExtension()
418
extensions = griffe.Extensions([type_ext])
419
420
loader = griffe.GriffeLoader(extensions=extensions)
421
package = loader.load("mypackage")
422
423
coverage = type_ext.get_type_coverage()
424
print(f"Type annotation coverage: {coverage['function_coverage']}%")
425
print(f"Complex types found: {coverage['complex_types']}")
426
```
427
428
### Extension with Configuration
429
430
```python
431
import griffe
432
from griffe import Extension
433
434
class ConfigurableExtension(Extension):
435
"""Extension with comprehensive configuration options."""
436
437
def __init__(
438
self,
439
track_private=False,
440
ignore_patterns=None,
441
output_file=None,
442
**options
443
):
444
super().__init__(**options)
445
self.track_private = track_private
446
self.ignore_patterns = ignore_patterns or []
447
self.output_file = output_file
448
self.data = []
449
450
def should_process(self, obj_path: str) -> bool:
451
"""Check if object should be processed based on configuration."""
452
# Skip private objects if not tracking them
453
if not self.track_private and any(part.startswith("_") for part in obj_path.split(".")):
454
return False
455
456
# Skip ignored patterns
457
if any(pattern in obj_path for pattern in self.ignore_patterns):
458
return False
459
460
return True
461
462
def on_function_loaded(self, *, func):
463
if self.should_process(func.path):
464
self.data.append({
465
"type": "function",
466
"path": func.path,
467
"parameters": len(func.parameters),
468
"has_docstring": func.docstring is not None
469
})
470
471
def on_class_loaded(self, *, cls):
472
if self.should_process(cls.path):
473
self.data.append({
474
"type": "class",
475
"path": cls.path,
476
"methods": len(cls.methods),
477
"attributes": len(cls.attributes),
478
"bases": len(cls.bases)
479
})
480
481
def finalize(self):
482
"""Called after all loading is complete."""
483
if self.output_file:
484
import json
485
with open(self.output_file, "w") as f:
486
json.dump(self.data, f, indent=2)
487
print(f"Extension data written to {self.output_file}")
488
489
# Configure and use extension
490
ext = ConfigurableExtension(
491
track_private=False,
492
ignore_patterns=["test_", "_internal"],
493
output_file="analysis_results.json"
494
)
495
496
extensions = griffe.Extensions([ext])
497
loader = griffe.GriffeLoader(extensions=extensions)
498
package = loader.load("mypackage")
499
500
# Finalize extension processing
501
ext.finalize()
502
```
503
504
## Advanced Extension Patterns
505
506
### Multi-Phase Extension
507
508
```python
509
import griffe
510
from griffe import Extension
511
512
class MultiPhaseExtension(Extension):
513
"""Extension that processes objects in multiple phases."""
514
515
def __init__(self, **options):
516
super().__init__(**options)
517
self.phase = "collection"
518
self.collected_objects = {"classes": [], "functions": []}
519
self.relationships = []
520
521
def on_class_loaded(self, *, cls):
522
if self.phase == "collection":
523
self.collected_objects["classes"].append(cls)
524
525
def on_function_loaded(self, *, func):
526
if self.phase == "collection":
527
self.collected_objects["functions"].append(func)
528
529
def on_package_loaded(self, *, pkg):
530
"""Switch to relationship analysis phase."""
531
if self.phase == "collection":
532
self.phase = "analysis"
533
self._analyze_relationships()
534
535
def _analyze_relationships(self):
536
"""Analyze relationships between collected objects."""
537
for cls in self.collected_objects["classes"]:
538
# Check inheritance relationships
539
for base in cls.bases:
540
base_name = str(base)
541
matching_classes = [
542
c for c in self.collected_objects["classes"]
543
if c.name == base_name.split(".")[-1]
544
]
545
if matching_classes:
546
self.relationships.append({
547
"type": "inheritance",
548
"child": cls.path,
549
"parent": matching_classes[0].path
550
})
551
552
print(f"Found {len(self.relationships)} relationships")
553
554
# Use multi-phase extension
555
multi_ext = MultiPhaseExtension()
556
extensions = griffe.Extensions([multi_ext])
557
558
loader = griffe.GriffeLoader(extensions=extensions)
559
package = loader.load("mypackage")
560
561
print("Relationships:", multi_ext.relationships)
562
```
563
564
## Types
565
566
```python { .api }
567
from typing import Any, Iterator, Union
568
569
# Extension type specifications
570
LoadableExtensionType = Union[
571
str, # Extension name or import path
572
type[Extension], # Extension class
573
Extension, # Extension instance
574
dict[str, Any] # Extension configuration
575
]
576
577
# Core extension classes
578
class Extension: ...
579
class Extensions: ...
580
581
# Built-in extensions registry
582
builtin_extensions: dict[str, type[Extension]]
583
584
# Built-in extension classes
585
class DataclassesExtension(Extension): ...
586
587
# Core object types from models
588
from griffe import Module, Class, Function, Attribute, Alias
589
```