Skip to content

Improve plugin-related error messages #3544

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

Merged
merged 3 commits into from
Jun 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)