Skip to content

Commit

Permalink
Improve plugin-related error messages (#3544)
Browse files Browse the repository at this point in the history
Address feedback by @gvanrossum in #3517.
  • Loading branch information
JukkaL authored Jun 20, 2017
1 parent fc974b1 commit 870d1d8
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 19 deletions.
54 changes: 43 additions & 11 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import hashlib
import json
import os.path
import re
import sys
import time
from os.path import dirname, basename
Expand Down Expand Up @@ -343,23 +344,29 @@ def load_custom_plugins(default_plugin: Plugin, options: Options, errors: Errors
back to default_plugin.
"""

if not options.config_file:
return default_plugin

line = find_config_file_line_number(options.config_file, 'mypy', 'plugins')
if line == -1:
line = 1 # We need to pick some line number that doesn't look too confusing

def plugin_error(message: str) -> None:
errors.report(0, 0, message)
errors.report(line, 0, message)
errors.raise_error()

errors.set_file(options.config_file, None)
custom_plugins = []
for plugin_path in options.plugins:
if options.config_file:
# Plugin paths are relative to the config file location.
plugin_path = os.path.join(os.path.dirname(options.config_file), plugin_path)
errors.set_file(plugin_path, None)
# Plugin paths are relative to the config file location.
plugin_path = os.path.join(os.path.dirname(options.config_file), plugin_path)

if not os.path.isfile(plugin_path):
plugin_error("Can't find plugin")
plugin_error("Can't find plugin '{}'".format(plugin_path))
plugin_dir = os.path.dirname(plugin_path)
fnam = os.path.basename(plugin_path)
if not fnam.endswith('.py'):
plugin_error("Plugin must have .py extension")
plugin_error("Plugin '{}' does not have a .py extension".format(fnam))
module_name = fnam[:-3]
import importlib
sys.path.insert(0, plugin_dir)
Expand All @@ -372,19 +379,21 @@ def plugin_error(message: str) -> None:
assert sys.path[0] == plugin_dir
del sys.path[0]
if not hasattr(m, 'plugin'):
plugin_error('Plugin does not define entry point function "plugin"')
plugin_error('Plugin \'{}\' does not define entry point function "plugin"'.format(
plugin_path))
try:
plugin_type = getattr(m, 'plugin')(__version__)
except Exception:
print('Error calling the plugin(version) entry point of {}\n'.format(plugin_path))
raise # Propagate to display traceback
if not isinstance(plugin_type, type):
plugin_error(
'Type object expected as the return value of "plugin" (got {!r})'.format(
plugin_type))
'Type object expected as the return value of "plugin"; got {!r} (in {})'.format(
plugin_type, plugin_path))
if not issubclass(plugin_type, Plugin):
plugin_error(
'Return value of "plugin" must be a subclass of "mypy.plugin.Plugin"')
'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))
except Exception:
Expand All @@ -397,6 +406,29 @@ def plugin_error(message: str) -> None:
return ChainedPlugin(options.python_version, custom_plugins + [default_plugin])


def find_config_file_line_number(path: str, section: str, setting_name: str) -> int:
"""Return the approximate location of setting_name within mypy config file.
Return -1 if can't determine the line unambiguously.
"""
in_desired_section = False
try:
results = []
with open(path) as f:
for i, line in enumerate(f):
line = line.strip()
if line.startswith('[') and line.endswith(']'):
current_section = line[1:-1].strip()
in_desired_section = (current_section == section)
elif in_desired_section and re.match(r'{}\s*='.format(setting_name), line):
results.append(i + 1)
if len(results) == 1:
return results[0]
except OSError:
pass
return -1


# TODO: Get rid of all_types. It's not used except for one log message.
# Maybe we could instead publish a map from module ID to its type_map.
class BuildManager:
Expand Down
30 changes: 22 additions & 8 deletions test-data/unit/check-custom-plugin.test
Original file line number Diff line number Diff line change
Expand Up @@ -30,35 +30,49 @@ plugins=<ROOT>/test-data/unit/plugins/fnplugin.py,
[[mypy]
plugins=missing.py
[out]
tmp/missing.py:0: error: Can't find plugin
tmp/mypy.ini:2: error: Can't find plugin 'tmp/missing.py'
--' (work around syntax highlighting)

[case testMultipleSectionsDefinePlugin]
# flags: --config-file tmp/mypy.ini
[file mypy.ini]
[[acme]
plugins=acmeplugin
[[mypy]
plugins=missing.py
[[another]
plugins=another_plugin
[out]
tmp/mypy.ini:4: error: Can't find plugin 'tmp/missing.py'
--' (work around syntax highlighting)

[case testInvalidPluginExtension]
# flags: --config-file tmp/mypy.ini
[file mypy.ini]
[[mypy]
plugins=badext.pyi
[file badext.pyi]
plugins=dir/badext.pyi
[file dir/badext.pyi]
[out]
tmp/badext.pyi:0: error: Plugin must have .py extension
tmp/mypy.ini:2: error: Plugin 'badext.pyi' does not have a .py extension

[case testMissingPluginEntryPoint]
# flags: --config-file tmp/mypy.ini
[file mypy.ini]
[[mypy]
plugins=<ROOT>/test-data/unit/plugins/noentry.py
plugins = <ROOT>/test-data/unit/plugins/noentry.py
[out]
<ROOT>/test-data/unit/plugins/noentry.py:0: error: Plugin does not define entry point function "plugin"
tmp/mypy.ini:2: error: Plugin '<ROOT>/test-data/unit/plugins/noentry.py' does not define entry point function "plugin"

[case testInvalidPluginEntryPointReturnValue]
# flags: --config-file tmp/mypy.ini
def f(): pass
f()
[file mypy.ini]
[[mypy]

plugins=<ROOT>/test-data/unit/plugins/badreturn.py
[out]
<ROOT>/test-data/unit/plugins/badreturn.py:0: error: Type object expected as the return value of "plugin" (got None)
tmp/mypy.ini:3: error: Type object expected as the return value of "plugin"; got None (in <ROOT>/test-data/unit/plugins/badreturn.py)

[case testInvalidPluginEntryPointReturnValue2]
# flags: --config-file tmp/mypy.ini
Expand All @@ -68,4 +82,4 @@ f()
[[mypy]
plugins=<ROOT>/test-data/unit/plugins/badreturn2.py
[out]
<ROOT>/test-data/unit/plugins/badreturn2.py:0: error: Return value of "plugin" must be a subclass of "mypy.plugin.Plugin"
tmp/mypy.ini:2: error: Return value of "plugin" must be a subclass of "mypy.plugin.Plugin" (in <ROOT>/test-data/unit/plugins/badreturn2.py)

0 comments on commit 870d1d8

Please sign in to comment.