0
# Plugin System
1
2
Extensible plugin architecture for customizing type checking behavior and adding support for specific libraries or frameworks. Mypy's plugin system allows deep integration with third-party libraries and custom type checking logic.
3
4
## Capabilities
5
6
### Core Plugin Classes
7
8
Base classes for creating mypy plugins that customize type checking behavior.
9
10
```python { .api }
11
class Plugin:
12
"""
13
Base class for mypy plugins.
14
15
Plugins can customize various aspects of type checking by providing
16
hooks that are called during different phases of analysis.
17
18
Methods to override:
19
- get_type_analyze_hook(self, fullname: str) -> Callable | None
20
- get_function_hook(self, fullname: str) -> Callable | None
21
- get_method_hook(self, fullname: str) -> Callable | None
22
- get_attribute_hook(self, fullname: str) -> Callable | None
23
- get_class_decorator_hook(self, fullname: str) -> Callable | None
24
- get_metaclass_hook(self, fullname: str) -> Callable | None
25
- get_base_class_hook(self, fullname: str) -> Callable | None
26
"""
27
28
def __init__(self, options: Options):
29
"""Initialize plugin with mypy options."""
30
31
class CommonPluginApi:
32
"""
33
Common API available to plugin callbacks.
34
35
Provides access to type analysis utilities, name lookup,
36
and type construction functions used by plugins.
37
38
Attributes:
39
- modules: dict[str, MypyFile] - Loaded modules
40
- msg: MessageBuilder - Error reporting
41
- options: Options - Mypy configuration
42
43
Methods:
44
- named_generic_type(name: str, args: list[Type]) -> Instance
45
- named_type(name: str) -> Instance
46
- lookup_fully_qualified(name: str) -> SymbolTableNode | None
47
- fail(msg: str, ctx: Context) -> None
48
"""
49
50
class SemanticAnalyzerPluginInterface(CommonPluginApi):
51
"""
52
API available during semantic analysis phase.
53
54
Used for plugins that need to analyze code structure,
55
modify AST nodes, or add new symbol table entries.
56
57
Additional methods:
58
- add_symbol_table_node(name: str, stnode: SymbolTableNode) -> None
59
- lookup_current_scope(name: str) -> SymbolTableNode | None
60
- defer_node(node: Node, enclosing_class: TypeInfo | None) -> None
61
"""
62
63
class CheckerPluginInterface(CommonPluginApi):
64
"""
65
API available during type checking phase.
66
67
Used for plugins that need to perform custom type checking,
68
validate specific patterns, or integrate with type inference.
69
70
Additional methods:
71
- check_subtype(left: Type, right: Type, ctx: Context) -> bool
72
- type_check_expr(expr: Expression, type_context: Type | None) -> Type
73
"""
74
```
75
76
### Plugin Context Classes
77
78
Context objects passed to plugin hooks containing information about the analysis context.
79
80
```python { .api }
81
class FunctionContext:
82
"""
83
Context for function call analysis hooks.
84
85
Attributes:
86
- default_return_type: Type - Default return type
87
- arg_types: list[list[Type]] - Argument types for each argument
88
- arg_names: list[list[str | None]] - Argument names
89
- callee_type: Type - Type of the function being called
90
- context: Context - AST context for error reporting
91
- api: CheckerPluginInterface - Type checker API
92
"""
93
94
class AttributeContext:
95
"""
96
Context for attribute access analysis hooks.
97
98
Attributes:
99
- default_attr_type: Type - Default attribute type
100
- type: Type - Type of the object being accessed
101
- context: Context - AST context
102
- api: CheckerPluginInterface - Type checker API
103
"""
104
105
class ClassDefContext:
106
"""
107
Context for class definition analysis hooks.
108
109
Attributes:
110
- cls: ClassDef - Class definition AST node
111
- reason: Type - Reason for hook invocation
112
- api: SemanticAnalyzerPluginInterface - Semantic analyzer API
113
"""
114
115
class BaseClassContext:
116
"""
117
Context for base class analysis hooks.
118
119
Attributes:
120
- cls: ClassDef - Class definition
121
- arg: Expression - Base class expression
122
- default_base: Type - Default base class type
123
- api: SemanticAnalyzerPluginInterface - API access
124
"""
125
```
126
127
## Built-in Plugins
128
129
### Default Plugin
130
131
Core plugin providing built-in type checking functionality.
132
133
```python { .api }
134
class DefaultPlugin(Plugin):
135
"""
136
Default plugin with built-in type handling.
137
138
Provides standard type checking for:
139
- Built-in functions and types
140
- Standard library modules
141
- Common Python patterns
142
- Generic type instantiation
143
"""
144
```
145
146
### Library-Specific Plugins
147
148
Pre-built plugins for popular Python libraries.
149
150
```python { .api }
151
# Available built-in plugins in mypy.plugins:
152
153
class AttrsPlugin(Plugin):
154
"""Support for attrs library decorators and classes."""
155
156
class DataclassesPlugin(Plugin):
157
"""Support for dataclasses with proper type inference."""
158
159
class EnumsPlugin(Plugin):
160
"""Enhanced support for enum.Enum classes."""
161
162
class FunctoolsPlugin(Plugin):
163
"""Support for functools decorators like @lru_cache."""
164
165
class CtypesPlugin(Plugin):
166
"""Support for ctypes library type checking."""
167
168
class SqlAlchemyPlugin(Plugin):
169
"""Support for SQLAlchemy ORM type checking."""
170
```
171
172
## Creating Custom Plugins
173
174
### Basic Plugin Structure
175
176
```python
177
from mypy.plugin import Plugin, FunctionContext
178
from mypy.types import Type, Instance
179
from mypy.nodes import ARG_POS, Argument, Var, PassStmt
180
181
class CustomPlugin(Plugin):
182
"""Example custom plugin for specialized type checking."""
183
184
def get_function_hook(self, fullname: str):
185
"""Return hook for specific function calls."""
186
if fullname == "mylib.special_function":
187
return self.handle_special_function
188
elif fullname == "mylib.create_instance":
189
return self.handle_create_instance
190
return None
191
192
def handle_special_function(self, ctx: FunctionContext) -> Type:
193
"""Custom handling for mylib.special_function."""
194
# Validate arguments
195
if len(ctx.arg_types) != 2:
196
ctx.api.fail("special_function requires exactly 2 arguments", ctx.context)
197
return ctx.default_return_type
198
199
# Check first argument is string
200
first_arg = ctx.arg_types[0][0] if ctx.arg_types[0] else None
201
if not isinstance(first_arg, Instance) or first_arg.type.fullname != 'builtins.str':
202
ctx.api.fail("First argument must be a string", ctx.context)
203
204
# Return custom type based on analysis
205
return ctx.api.named_type('mylib.SpecialResult')
206
207
def handle_create_instance(self, ctx: FunctionContext) -> Type:
208
"""Custom factory function handling."""
209
if ctx.arg_types and ctx.arg_types[0]:
210
# Return instance of type specified in first argument
211
type_arg = ctx.arg_types[0][0]
212
if isinstance(type_arg, Instance):
213
return type_arg
214
215
return ctx.default_return_type
216
217
# Plugin entry point
218
def plugin(version: str):
219
"""Entry point for mypy plugin discovery."""
220
return CustomPlugin
221
```
222
223
### Advanced Plugin Features
224
225
```python
226
from mypy.plugin import Plugin, ClassDefContext, BaseClassContext
227
from mypy.types import Type, Instance, CallableType
228
from mypy.nodes import ClassDef, FuncDef, Decorator
229
230
class AdvancedPlugin(Plugin):
231
"""Advanced plugin with class and decorator handling."""
232
233
def get_class_decorator_hook(self, fullname: str):
234
"""Handle class decorators."""
235
if fullname == "mylib.special_class":
236
return self.handle_special_class_decorator
237
return None
238
239
def get_base_class_hook(self, fullname: str):
240
"""Handle specific base classes."""
241
if fullname == "mylib.BaseModel":
242
return self.handle_base_model
243
return None
244
245
def handle_special_class_decorator(self, ctx: ClassDefContext) -> None:
246
"""Process @special_class decorator."""
247
# Add special methods to the class
248
self.add_magic_method(ctx.cls, "__special__",
249
ctx.api.named_type('builtins.str'))
250
251
def handle_base_model(self, ctx: BaseClassContext) -> Type:
252
"""Handle BaseModel inheritance."""
253
# Analyze class for special fields
254
if isinstance(ctx.cls, ClassDef):
255
self.process_model_fields(ctx.cls, ctx.api)
256
257
return ctx.default_base
258
259
def add_magic_method(self, cls: ClassDef, method_name: str,
260
return_type: Type) -> None:
261
"""Add a magic method to class definition."""
262
# Create method signature
263
method_type = CallableType(
264
arg_types=[Instance(cls.info, [])], # self parameter
265
arg_kinds=[ARG_POS],
266
arg_names=['self'],
267
return_type=return_type,
268
fallback=self.lookup_typeinfo('builtins.function')
269
)
270
271
# Add to symbol table
272
method_node = FuncDef(method_name, [], None, None)
273
method_node.type = method_type
274
cls.info.names[method_name] = method_node
275
276
def process_model_fields(self, cls: ClassDef, api) -> None:
277
"""Process model field definitions."""
278
for stmt in cls.defs.body:
279
if isinstance(stmt, AssignmentStmt):
280
# Analyze field assignments
281
self.analyze_field_assignment(stmt, api)
282
283
def plugin(version: str):
284
return AdvancedPlugin
285
```
286
287
### Plugin with Type Analysis
288
289
```python
290
from mypy.plugin import Plugin, AttributeContext
291
from mypy.types import Type, Instance, AnyType, TypeOfAny
292
293
class TypeAnalysisPlugin(Plugin):
294
"""Plugin demonstrating type analysis capabilities."""
295
296
def get_attribute_hook(self, fullname: str):
297
"""Handle attribute access."""
298
if fullname == "mylib.DynamicObject.__getattr__":
299
return self.handle_dynamic_getattr
300
return None
301
302
def handle_dynamic_getattr(self, ctx: AttributeContext) -> Type:
303
"""Handle dynamic attribute access."""
304
# Analyze the attribute name
305
attr_name = self.get_attribute_name(ctx)
306
307
if attr_name and attr_name.startswith('computed_'):
308
# Return specific type for computed attributes
309
return ctx.api.named_type('builtins.float')
310
elif attr_name and attr_name.startswith('cached_'):
311
# Return cached value type
312
return self.get_cached_type(attr_name, ctx)
313
314
# Default to Any for unknown dynamic attributes
315
return AnyType(TypeOfAny.from_error)
316
317
def get_attribute_name(self, ctx: AttributeContext) -> str | None:
318
"""Extract attribute name from context."""
319
# This would need to analyze the AST context
320
# Implementation depends on specific use case
321
return None
322
323
def get_cached_type(self, attr_name: str, ctx: AttributeContext) -> Type:
324
"""Determine type for cached attributes."""
325
# Custom logic for determining cached value types
326
cache_map = {
327
'cached_count': ctx.api.named_type('builtins.int'),
328
'cached_name': ctx.api.named_type('builtins.str'),
329
'cached_data': ctx.api.named_type('builtins.list')
330
}
331
332
return cache_map.get(attr_name, AnyType(TypeOfAny.from_error))
333
334
def plugin(version: str):
335
return TypeAnalysisPlugin
336
```
337
338
## Plugin Configuration and Loading
339
340
### Plugin Entry Points
341
342
```python
343
# setup.py or pyproject.toml configuration for plugin distribution
344
from setuptools import setup
345
346
setup(
347
name="mypy-custom-plugin",
348
entry_points={
349
"mypy.plugins": [
350
"custom_plugin = mypy_custom_plugin.plugin:plugin"
351
]
352
}
353
)
354
```
355
356
### Plugin Loading in mypy.ini
357
358
```ini
359
[mypy]
360
plugins = mypy_custom_plugin.plugin, another_plugin
361
362
[mypy-mylib.*]
363
# Plugin-specific configuration
364
ignore_errors = false
365
```
366
367
### Plugin Loading Programmatically
368
369
```python
370
from mypy.build import build, BuildSource
371
from mypy.options import Options
372
373
# Load plugin programmatically
374
options = Options()
375
options.plugins = ['mypy_custom_plugin.plugin']
376
377
# Custom plugin instance
378
plugin_instance = CustomPlugin(options)
379
380
sources = [BuildSource("myfile.py", None, None)]
381
result = build(sources, options, extra_plugins=[plugin_instance])
382
```
383
384
## Testing Plugins
385
386
### Plugin Test Framework
387
388
```python
389
import tempfile
390
import os
391
from mypy import api
392
from mypy.test.helpers import Suite
393
394
class PluginTestCase:
395
"""Test framework for mypy plugins."""
396
397
def run_with_plugin(self, source_code: str, plugin_path: str) -> tuple[str, str, int]:
398
"""Run mypy with plugin on source code."""
399
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
400
f.write(source_code)
401
temp_file = f.name
402
403
try:
404
# Run mypy with plugin
405
result = api.run([
406
'--plugins', plugin_path,
407
'--show-error-codes',
408
temp_file
409
])
410
return result
411
finally:
412
os.unlink(temp_file)
413
414
def assert_no_errors(self, result: tuple[str, str, int]):
415
"""Assert that mypy found no errors."""
416
stdout, stderr, exit_code = result
417
assert exit_code == 0, f"Expected no errors, got: {stderr}"
418
419
def assert_error_contains(self, result: tuple[str, str, int],
420
expected_message: str):
421
"""Assert that error output contains expected message."""
422
stdout, stderr, exit_code = result
423
assert expected_message in stderr, f"Expected '{expected_message}' in: {stderr}"
424
425
# Usage
426
def test_custom_plugin():
427
"""Test custom plugin functionality."""
428
source = '''
429
from mylib import special_function
430
431
result = special_function("hello", 42) # Should pass
432
result2 = special_function(123, "world") # Should fail
433
'''
434
435
test_case = PluginTestCase()
436
result = test_case.run_with_plugin(source, "mypy_custom_plugin.plugin")
437
438
# Should have one error for the second call
439
test_case.assert_error_contains(result, "First argument must be a string")
440
```
441
442
### Integration Testing
443
444
```python
445
import pytest
446
from mypy import api
447
448
class TestPluginIntegration:
449
"""Integration tests for plugin with real codebases."""
450
451
@pytest.fixture
452
def sample_project(self, tmp_path):
453
"""Create sample project for testing."""
454
# Create project structure
455
(tmp_path / "mylib").mkdir()
456
(tmp_path / "mylib" / "__init__.py").write_text("")
457
458
(tmp_path / "mylib" / "core.py").write_text('''
459
class SpecialResult:
460
def __init__(self, value: str):
461
self.value = value
462
463
def special_function(name: str, count: int) -> SpecialResult:
464
return SpecialResult(f"{name}_{count}")
465
''')
466
467
(tmp_path / "main.py").write_text('''
468
from mylib.core import special_function
469
470
result = special_function("test", 5)
471
print(result.value)
472
''')
473
474
return tmp_path
475
476
def test_plugin_with_project(self, sample_project):
477
"""Test plugin with complete project."""
478
os.chdir(sample_project)
479
480
result = api.run([
481
'--plugins', 'mypy_custom_plugin.plugin',
482
'main.py'
483
])
484
485
stdout, stderr, exit_code = result
486
assert exit_code == 0, f"Plugin failed on project: {stderr}"
487
```
488
489
## Plugin Best Practices
490
491
### Performance Considerations
492
493
```python
494
class EfficientPlugin(Plugin):
495
"""Example of performance-conscious plugin design."""
496
497
def __init__(self, options):
498
super().__init__(options)
499
# Cache expensive computations
500
self._type_cache = {}
501
self._analyzed_classes = set()
502
503
def get_function_hook(self, fullname: str):
504
# Use early returns to avoid unnecessary work
505
if not fullname.startswith('mylib.'):
506
return None
507
508
# Cache hook lookups
509
if fullname not in self._hook_cache:
510
self._hook_cache[fullname] = self._compute_hook(fullname)
511
512
return self._hook_cache[fullname]
513
514
def handle_expensive_operation(self, ctx: FunctionContext) -> Type:
515
"""Cache expensive type computations."""
516
cache_key = (ctx.callee_type, tuple(str(t) for t in ctx.arg_types[0]))
517
518
if cache_key in self._type_cache:
519
return self._type_cache[cache_key]
520
521
# Perform expensive computation
522
result = self._compute_type(ctx)
523
self._type_cache[cache_key] = result
524
return result
525
```
526
527
### Error Handling in Plugins
528
529
```python
530
class RobustPlugin(Plugin):
531
"""Plugin with proper error handling."""
532
533
def handle_function_call(self, ctx: FunctionContext) -> Type:
534
"""Safely handle function calls with error recovery."""
535
try:
536
# Validate context
537
if not ctx.arg_types:
538
ctx.api.fail("Missing arguments", ctx.context)
539
return ctx.default_return_type
540
541
# Perform analysis
542
return self._analyze_call(ctx)
543
544
except Exception as e:
545
# Log error and fall back to default behavior
546
ctx.api.fail(f"Plugin error: {e}", ctx.context)
547
return ctx.default_return_type
548
549
def _analyze_call(self, ctx: FunctionContext) -> Type:
550
"""Internal analysis with proper error handling."""
551
# Implementation with validation at each step
552
pass
553
```