-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support additional plugin hooks #3534
Changes from all commits
437509c
77bb797
6471eaa
ae70141
3283cbf
9b3322d
01fd4f4
f24abbf
6aa789c
da74c58
698bdef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -172,8 +172,9 @@ def build(sources: List[BuildSource], | |
lib_path.insert(0, alt_lib_path) | ||
|
||
reports = Reports(data_dir, options.report_dirs) | ||
|
||
source_set = BuildSourceSet(sources) | ||
errors = Errors(options.show_error_context, options.show_column_numbers) | ||
plugin = load_plugins(options, errors) | ||
|
||
# Construct a build manager object to hold state during the build. | ||
# | ||
|
@@ -184,9 +185,8 @@ def build(sources: List[BuildSource], | |
reports=reports, | ||
options=options, | ||
version_id=__version__, | ||
plugin=DefaultPlugin(options.python_version)) | ||
|
||
manager.plugin = load_custom_plugins(manager.plugin, options, manager.errors) | ||
plugin=plugin, | ||
errors=errors) | ||
|
||
try: | ||
graph = dispatch(sources, manager) | ||
|
@@ -337,13 +337,14 @@ def import_priority(imp: ImportBase, toplevel_priority: int) -> int: | |
return toplevel_priority | ||
|
||
|
||
def load_custom_plugins(default_plugin: Plugin, options: Options, errors: Errors) -> Plugin: | ||
"""Load custom plugins if any are configured. | ||
def load_plugins(options: Options, errors: Errors) -> Plugin: | ||
"""Load all configured plugins. | ||
|
||
Return a plugin that chains all custom plugins (if any) and falls | ||
back to default_plugin. | ||
Return a plugin that encapsulates all plugins chained together. Always | ||
at least include the default plugin (it's last in the chain). | ||
""" | ||
|
||
default_plugin = DefaultPlugin(options) # type: Plugin | ||
if not options.config_file: | ||
return default_plugin | ||
|
||
|
@@ -355,8 +356,8 @@ def plugin_error(message: str) -> None: | |
errors.report(line, 0, message) | ||
errors.raise_error() | ||
|
||
custom_plugins = [] # type: List[Plugin] | ||
errors.set_file(options.config_file, None) | ||
custom_plugins = [] | ||
for plugin_path in options.plugins: | ||
# Plugin paths are relative to the config file location. | ||
plugin_path = os.path.join(os.path.dirname(options.config_file), plugin_path) | ||
|
@@ -395,15 +396,12 @@ def plugin_error(message: str) -> None: | |
'Return value of "plugin" must be a subclass of "mypy.plugin.Plugin" ' | ||
'(in {})'.format(plugin_path)) | ||
try: | ||
custom_plugins.append(plugin_type(options.python_version)) | ||
custom_plugins.append(plugin_type(options)) | ||
except Exception: | ||
print('Error constructing plugin instance of {}\n'.format(plugin_type.__name__)) | ||
raise # Propagate to display traceback | ||
if not custom_plugins: | ||
return default_plugin | ||
else: | ||
# Custom plugins take precendence over built-in plugins. | ||
return ChainedPlugin(options.python_version, custom_plugins + [default_plugin]) | ||
# Custom plugins take precedence over the default plugin. | ||
return ChainedPlugin(options, custom_plugins + [default_plugin]) | ||
|
||
|
||
def find_config_file_line_number(path: str, section: str, setting_name: str) -> int: | ||
|
@@ -447,12 +445,12 @@ class BuildManager: | |
semantic_analyzer_pass3: | ||
Semantic analyzer, pass 3 | ||
all_types: Map {Expression: Type} collected from all modules | ||
errors: Used for reporting all errors | ||
options: Build options | ||
missing_modules: Set of modules that could not be imported encountered so far | ||
stale_modules: Set of modules that needed to be rechecked | ||
version_id: The current mypy version (based on commit id when possible) | ||
plugin: Active mypy plugin(s) | ||
errors: Used for reporting all errors | ||
""" | ||
|
||
def __init__(self, data_dir: str, | ||
|
@@ -462,10 +460,11 @@ def __init__(self, data_dir: str, | |
reports: Reports, | ||
options: Options, | ||
version_id: str, | ||
plugin: Plugin) -> None: | ||
plugin: Plugin, | ||
errors: Errors) -> None: | ||
self.start_time = time.time() | ||
self.data_dir = data_dir | ||
self.errors = Errors(options.show_error_context, options.show_column_numbers) | ||
self.errors = errors | ||
self.errors.set_ignore_prefix(ignore_prefix) | ||
self.lib_path = tuple(lib_path) | ||
self.source_set = source_set | ||
|
@@ -474,8 +473,9 @@ def __init__(self, data_dir: str, | |
self.version_id = version_id | ||
self.modules = {} # type: Dict[str, MypyFile] | ||
self.missing_modules = set() # type: Set[str] | ||
self.plugin = plugin | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JukkaL you have |
||
self.semantic_analyzer = SemanticAnalyzer(self.modules, self.missing_modules, | ||
lib_path, self.errors) | ||
lib_path, self.errors, self.plugin) | ||
self.modules = self.semantic_analyzer.modules | ||
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors) | ||
self.all_types = {} # type: Dict[Expression, Type] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,7 +45,7 @@ | |
from mypy.util import split_module_names | ||
from mypy.typevars import fill_typevars | ||
from mypy.visitor import ExpressionVisitor | ||
from mypy.plugin import Plugin, PluginContext, MethodSignatureHook | ||
from mypy.plugin import Plugin, MethodContext, MethodSigContext, FunctionContext | ||
from mypy.typeanal import make_optional_type | ||
|
||
from mypy import experiments | ||
|
@@ -380,6 +380,13 @@ def apply_function_plugin(self, | |
context: Context) -> Type: | ||
"""Use special case logic to infer the return type of a specific named function/method. | ||
|
||
Caller must ensure that a plugin hook exists. There are two different cases: | ||
|
||
- If object_type is None, the caller must ensure that a function hook exists | ||
for fullname. | ||
- If object_type is not None, the caller must ensure that a method hook exists | ||
for fullname. | ||
|
||
Return the inferred return type. | ||
""" | ||
formal_arg_types = [[] for _ in range(num_formals)] # type: List[List[Type]] | ||
|
@@ -392,17 +399,21 @@ def apply_function_plugin(self, | |
# Apply function plugin | ||
callback = self.plugin.get_function_hook(fullname) | ||
assert callback is not None # Assume that caller ensures this | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still feels a bit awkward to me, but I agree that other ways to factor this out aren't much better. Maybe the docstring should just call out that there are two calling cases and that the caller should ensure the relevant callback isn't None? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated docstring |
||
return callback(formal_arg_types, formal_arg_exprs, inferred_ret_type, | ||
self.chk.named_generic_type) | ||
return callback( | ||
FunctionContext(formal_arg_types, inferred_ret_type, formal_arg_exprs, | ||
context, self.chk)) | ||
else: | ||
# Apply method plugin | ||
method_callback = self.plugin.get_method_hook(fullname) | ||
assert method_callback is not None # Assume that caller ensures this | ||
return method_callback(object_type, formal_arg_types, formal_arg_exprs, | ||
inferred_ret_type, self.create_plugin_context(context)) | ||
|
||
def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_type: Type, | ||
signature_hook: MethodSignatureHook) -> FunctionLike: | ||
return method_callback( | ||
MethodContext(object_type, formal_arg_types, | ||
inferred_ret_type, formal_arg_exprs, | ||
context, self.chk)) | ||
|
||
def apply_method_signature_hook( | ||
self, e: CallExpr, callee: FunctionLike, object_type: Type, | ||
signature_hook: Callable[[MethodSigContext], CallableType]) -> FunctionLike: | ||
"""Apply a plugin hook that may infer a more precise signature for a method.""" | ||
if isinstance(callee, CallableType): | ||
arg_kinds = e.arg_kinds | ||
|
@@ -417,8 +428,8 @@ def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_ | |
for formal, actuals in enumerate(formal_to_actual): | ||
for actual in actuals: | ||
formal_arg_exprs[formal].append(args[actual]) | ||
return signature_hook(object_type, formal_arg_exprs, callee, | ||
self.chk.named_generic_type) | ||
return signature_hook( | ||
MethodSigContext(object_type, formal_arg_exprs, callee, e, self.chk)) | ||
else: | ||
assert isinstance(callee, Overloaded) | ||
items = [] | ||
|
@@ -428,9 +439,6 @@ def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_ | |
items.append(adjusted) | ||
return Overloaded(items) | ||
|
||
def create_plugin_context(self, context: Context) -> PluginContext: | ||
return PluginContext(self.chk.named_generic_type, self.msg, context) | ||
|
||
def check_call_expr_with_callee_type(self, | ||
callee_type: Type, | ||
e: CallExpr, | ||
|
@@ -475,6 +483,8 @@ def check_call(self, callee: Type, args: List[Expression], | |
""" | ||
arg_messages = arg_messages or self.msg | ||
if isinstance(callee, CallableType): | ||
if callable_name is None and callee.name: | ||
callable_name = callee.name | ||
if (isinstance(callable_node, RefExpr) | ||
and callable_node.fullname in ('enum.Enum', 'enum.IntEnum', | ||
'enum.Flag', 'enum.IntFlag')): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add this to the docstring too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was mentioned already, but moved to another place where it might be easier to find.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In some future PR maybe we can remove the attributes from the docstring and instead use class-level annotations + comments for them.