Skip to content

Commit 81ec44b

Browse files
authored
Redesign plugins and add additional plugin hooks (#3534)
Instead of passing several arguments to hook function, always pass just a single object. This simplifies the signatures of hooks. Instead of passing callback functions to hooks, pass an object that implements a specific interface. These changes are intended to make it easier to write plugins, and to make it easier to evolve the plugin system. Adding extra attributes to context or extra methods to the internal interfaces doesn't require changes to existing plugins. Additional updates: * Add hook that overrides the inferred type of an instance attribute. In particular, this can be used to override the type of `__call__`. * Add hook for custom semantic analysis of types. This makes it possible to have some limited custom syntax for generic types. (We can extend the syntactic possibilities in the future.) * Use the name of a callable to decide which hook to apply. This makes it possible to use custom hooks for callables returned by functions.
1 parent 4bc10c7 commit 81ec44b

15 files changed

+458
-220
lines changed

mypy/build.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,9 @@ def build(sources: List[BuildSource],
172172
lib_path.insert(0, alt_lib_path)
173173

174174
reports = Reports(data_dir, options.report_dirs)
175-
176175
source_set = BuildSourceSet(sources)
176+
errors = Errors(options.show_error_context, options.show_column_numbers)
177+
plugin = load_plugins(options, errors)
177178

178179
# Construct a build manager object to hold state during the build.
179180
#
@@ -184,9 +185,8 @@ def build(sources: List[BuildSource],
184185
reports=reports,
185186
options=options,
186187
version_id=__version__,
187-
plugin=DefaultPlugin(options.python_version))
188-
189-
manager.plugin = load_custom_plugins(manager.plugin, options, manager.errors)
188+
plugin=plugin,
189+
errors=errors)
190190

191191
try:
192192
graph = dispatch(sources, manager)
@@ -337,13 +337,14 @@ def import_priority(imp: ImportBase, toplevel_priority: int) -> int:
337337
return toplevel_priority
338338

339339

340-
def load_custom_plugins(default_plugin: Plugin, options: Options, errors: Errors) -> Plugin:
341-
"""Load custom plugins if any are configured.
340+
def load_plugins(options: Options, errors: Errors) -> Plugin:
341+
"""Load all configured plugins.
342342
343-
Return a plugin that chains all custom plugins (if any) and falls
344-
back to default_plugin.
343+
Return a plugin that encapsulates all plugins chained together. Always
344+
at least include the default plugin (it's last in the chain).
345345
"""
346346

347+
default_plugin = DefaultPlugin(options) # type: Plugin
347348
if not options.config_file:
348349
return default_plugin
349350

@@ -355,8 +356,8 @@ def plugin_error(message: str) -> None:
355356
errors.report(line, 0, message)
356357
errors.raise_error()
357358

359+
custom_plugins = [] # type: List[Plugin]
358360
errors.set_file(options.config_file, None)
359-
custom_plugins = []
360361
for plugin_path in options.plugins:
361362
# Plugin paths are relative to the config file location.
362363
plugin_path = os.path.join(os.path.dirname(options.config_file), plugin_path)
@@ -395,15 +396,12 @@ def plugin_error(message: str) -> None:
395396
'Return value of "plugin" must be a subclass of "mypy.plugin.Plugin" '
396397
'(in {})'.format(plugin_path))
397398
try:
398-
custom_plugins.append(plugin_type(options.python_version))
399+
custom_plugins.append(plugin_type(options))
399400
except Exception:
400401
print('Error constructing plugin instance of {}\n'.format(plugin_type.__name__))
401402
raise # Propagate to display traceback
402-
if not custom_plugins:
403-
return default_plugin
404-
else:
405-
# Custom plugins take precendence over built-in plugins.
406-
return ChainedPlugin(options.python_version, custom_plugins + [default_plugin])
403+
# Custom plugins take precedence over the default plugin.
404+
return ChainedPlugin(options, custom_plugins + [default_plugin])
407405

408406

409407
def find_config_file_line_number(path: str, section: str, setting_name: str) -> int:
@@ -447,12 +445,12 @@ class BuildManager:
447445
semantic_analyzer_pass3:
448446
Semantic analyzer, pass 3
449447
all_types: Map {Expression: Type} collected from all modules
450-
errors: Used for reporting all errors
451448
options: Build options
452449
missing_modules: Set of modules that could not be imported encountered so far
453450
stale_modules: Set of modules that needed to be rechecked
454451
version_id: The current mypy version (based on commit id when possible)
455452
plugin: Active mypy plugin(s)
453+
errors: Used for reporting all errors
456454
"""
457455

458456
def __init__(self, data_dir: str,
@@ -462,10 +460,11 @@ def __init__(self, data_dir: str,
462460
reports: Reports,
463461
options: Options,
464462
version_id: str,
465-
plugin: Plugin) -> None:
463+
plugin: Plugin,
464+
errors: Errors) -> None:
466465
self.start_time = time.time()
467466
self.data_dir = data_dir
468-
self.errors = Errors(options.show_error_context, options.show_column_numbers)
467+
self.errors = errors
469468
self.errors.set_ignore_prefix(ignore_prefix)
470469
self.lib_path = tuple(lib_path)
471470
self.source_set = source_set
@@ -474,8 +473,9 @@ def __init__(self, data_dir: str,
474473
self.version_id = version_id
475474
self.modules = {} # type: Dict[str, MypyFile]
476475
self.missing_modules = set() # type: Set[str]
476+
self.plugin = plugin
477477
self.semantic_analyzer = SemanticAnalyzer(self.modules, self.missing_modules,
478-
lib_path, self.errors)
478+
lib_path, self.errors, self.plugin)
479479
self.modules = self.semantic_analyzer.modules
480480
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
481481
self.all_types = {} # type: Dict[Expression, Type]

mypy/checker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
from mypy.binder import ConditionalTypeBinder, get_declaration
5858
from mypy.meet import is_overlapping_types
5959
from mypy.options import Options
60-
from mypy.plugin import Plugin
60+
from mypy.plugin import Plugin, CheckerPluginInterface
6161

6262
from mypy import experiments
6363

@@ -80,7 +80,7 @@
8080
])
8181

8282

83-
class TypeChecker(NodeVisitor[None]):
83+
class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
8484
"""Mypy type checker.
8585
8686
Type check mypy source files that have been semantically analyzed.

mypy/checkexpr.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from mypy.util import split_module_names
4646
from mypy.typevars import fill_typevars
4747
from mypy.visitor import ExpressionVisitor
48-
from mypy.plugin import Plugin, PluginContext, MethodSignatureHook
48+
from mypy.plugin import Plugin, MethodContext, MethodSigContext, FunctionContext
4949
from mypy.typeanal import make_optional_type
5050

5151
from mypy import experiments
@@ -380,6 +380,13 @@ def apply_function_plugin(self,
380380
context: Context) -> Type:
381381
"""Use special case logic to infer the return type of a specific named function/method.
382382
383+
Caller must ensure that a plugin hook exists. There are two different cases:
384+
385+
- If object_type is None, the caller must ensure that a function hook exists
386+
for fullname.
387+
- If object_type is not None, the caller must ensure that a method hook exists
388+
for fullname.
389+
383390
Return the inferred return type.
384391
"""
385392
formal_arg_types = [[] for _ in range(num_formals)] # type: List[List[Type]]
@@ -392,17 +399,21 @@ def apply_function_plugin(self,
392399
# Apply function plugin
393400
callback = self.plugin.get_function_hook(fullname)
394401
assert callback is not None # Assume that caller ensures this
395-
return callback(formal_arg_types, formal_arg_exprs, inferred_ret_type,
396-
self.chk.named_generic_type)
402+
return callback(
403+
FunctionContext(formal_arg_types, inferred_ret_type, formal_arg_exprs,
404+
context, self.chk))
397405
else:
398406
# Apply method plugin
399407
method_callback = self.plugin.get_method_hook(fullname)
400408
assert method_callback is not None # Assume that caller ensures this
401-
return method_callback(object_type, formal_arg_types, formal_arg_exprs,
402-
inferred_ret_type, self.create_plugin_context(context))
403-
404-
def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_type: Type,
405-
signature_hook: MethodSignatureHook) -> FunctionLike:
409+
return method_callback(
410+
MethodContext(object_type, formal_arg_types,
411+
inferred_ret_type, formal_arg_exprs,
412+
context, self.chk))
413+
414+
def apply_method_signature_hook(
415+
self, e: CallExpr, callee: FunctionLike, object_type: Type,
416+
signature_hook: Callable[[MethodSigContext], CallableType]) -> FunctionLike:
406417
"""Apply a plugin hook that may infer a more precise signature for a method."""
407418
if isinstance(callee, CallableType):
408419
arg_kinds = e.arg_kinds
@@ -417,8 +428,8 @@ def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_
417428
for formal, actuals in enumerate(formal_to_actual):
418429
for actual in actuals:
419430
formal_arg_exprs[formal].append(args[actual])
420-
return signature_hook(object_type, formal_arg_exprs, callee,
421-
self.chk.named_generic_type)
431+
return signature_hook(
432+
MethodSigContext(object_type, formal_arg_exprs, callee, e, self.chk))
422433
else:
423434
assert isinstance(callee, Overloaded)
424435
items = []
@@ -428,9 +439,6 @@ def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_
428439
items.append(adjusted)
429440
return Overloaded(items)
430441

431-
def create_plugin_context(self, context: Context) -> PluginContext:
432-
return PluginContext(self.chk.named_generic_type, self.msg, context)
433-
434442
def check_call_expr_with_callee_type(self,
435443
callee_type: Type,
436444
e: CallExpr,
@@ -475,6 +483,8 @@ def check_call(self, callee: Type, args: List[Expression],
475483
"""
476484
arg_messages = arg_messages or self.msg
477485
if isinstance(callee, CallableType):
486+
if callable_name is None and callee.name:
487+
callable_name = callee.name
478488
if (isinstance(callable_node, RefExpr)
479489
and callable_node.fullname in ('enum.Enum', 'enum.IntEnum',
480490
'enum.Flag', 'enum.IntFlag')):

mypy/checkmember.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from mypy.expandtype import expand_type_by_instance, expand_type, freshen_function_type_vars
1818
from mypy.infer import infer_type_arguments
1919
from mypy.typevars import fill_typevars
20+
from mypy.plugin import Plugin, AttributeContext
2021
from mypy import messages
2122
from mypy import subtypes
2223
MYPY = False
@@ -36,8 +37,8 @@ def analyze_member_access(name: str,
3637
not_ready_callback: Callable[[str, Context], None],
3738
msg: MessageBuilder, *,
3839
original_type: Type,
39-
override_info: TypeInfo = None,
40-
chk: 'mypy.checker.TypeChecker' = None) -> Type:
40+
chk: 'mypy.checker.TypeChecker',
41+
override_info: TypeInfo = None) -> Type:
4142
"""Return the type of attribute `name` of typ.
4243
4344
This is a general operation that supports various different variations:
@@ -77,7 +78,7 @@ def analyze_member_access(name: str,
7778
assert isinstance(method, OverloadedFuncDef)
7879
first_item = cast(Decorator, method.items[0])
7980
return analyze_var(name, first_item.var, typ, info, node, is_lvalue, msg,
80-
original_type, not_ready_callback)
81+
original_type, not_ready_callback, chk=chk)
8182
if is_lvalue:
8283
msg.cant_assign_to_method(node)
8384
signature = function_type(method, builtin_type('builtins.function'))
@@ -102,7 +103,7 @@ def analyze_member_access(name: str,
102103
# The base object has dynamic type.
103104
return AnyType()
104105
elif isinstance(typ, NoneTyp):
105-
if chk and chk.should_suppress_optional_error([typ]):
106+
if chk.should_suppress_optional_error([typ]):
106107
return AnyType()
107108
# The only attribute NoneType has are those it inherits from object
108109
return analyze_member_access(name, builtin_type('builtins.object'), node, is_lvalue,
@@ -200,7 +201,7 @@ def analyze_member_access(name: str,
200201
is_operator, builtin_type, not_ready_callback, msg,
201202
original_type=original_type, chk=chk)
202203

203-
if chk and chk.should_suppress_optional_error([typ]):
204+
if chk.should_suppress_optional_error([typ]):
204205
return AnyType()
205206
return msg.has_no_attr(original_type, typ, name, node)
206207

@@ -228,7 +229,7 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
228229

229230
if isinstance(v, Var):
230231
return analyze_var(name, v, itype, info, node, is_lvalue, msg,
231-
original_type, not_ready_callback)
232+
original_type, not_ready_callback, chk=chk)
232233
elif isinstance(v, FuncDef):
233234
assert False, "Did not expect a function"
234235
elif not v and name not in ['__getattr__', '__setattr__', '__getattribute__']:
@@ -270,7 +271,8 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
270271

271272
def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Context,
272273
is_lvalue: bool, msg: MessageBuilder, original_type: Type,
273-
not_ready_callback: Callable[[str, Context], None]) -> Type:
274+
not_ready_callback: Callable[[str, Context], None], *,
275+
chk: 'mypy.checker.TypeChecker') -> Type:
274276
"""Analyze access to an attribute via a Var node.
275277
276278
This is conceptually part of analyze_member_access and the arguments are similar.
@@ -289,6 +291,7 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont
289291
msg.read_only_property(name, info, node)
290292
if is_lvalue and var.is_classvar:
291293
msg.cant_assign_to_classvar(name, node)
294+
result = t
292295
if var.is_initialized_in_class and isinstance(t, FunctionLike) and not t.is_type_obj():
293296
if is_lvalue:
294297
if var.is_property:
@@ -308,15 +311,19 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont
308311
# A property cannot have an overloaded type => the cast
309312
# is fine.
310313
assert isinstance(signature, CallableType)
311-
return signature.ret_type
314+
result = signature.ret_type
312315
else:
313-
return signature
314-
return t
316+
result = signature
315317
else:
316318
if not var.is_ready:
317319
not_ready_callback(var.name(), node)
318320
# Implicit 'Any' type.
319-
return AnyType()
321+
result = AnyType()
322+
fullname = '{}.{}'.format(var.info.fullname(), name)
323+
hook = chk.plugin.get_attribute_hook(fullname)
324+
if hook:
325+
result = hook(AttributeContext(original_type, result, node, chk))
326+
return result
320327

321328

322329
def freeze_type_vars(member_type: Type) -> None:

0 commit comments

Comments
 (0)