8000 Improve plugin-related error messages by JukkaL · Pull Request #3544 · python/mypy · GitHub
[go: up one dir, main page]

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 1 commit
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
Next Next commit
Improve plugin-related error messages
Address feedback by @gvanrossum in #3517.
  • Loading branch information
JukkaL committed Jun 14, 2017
commit 156e204eafa0456f4d462e51f4bbd5a81eb27972
49 changes: 38 additions & 11 deletions mypy/build.py
8000
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, '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(
module_name))
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})'.format(
module_name, plugin_type))
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"'.format(
module_name))
try:
custom_plugins.append(plugin_type(options.python_version))
except Exception:
Expand All @@ -397,6 +406,24 @@ def plugin_error(message: str) -> None:
return ChainedPlugin(options.python_version, custom_plugins + [default_plugin])


def find_config_file_line_number(path: str, setting_name: str) -> int:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bummer that the ConfigFile class doesn't keep track of this. I wonder if you could at least look for sections so you only find the desired option in the [mypy] section?

"""Return the approximate location of setting_name within mypy config file.

Return -1 if can't determine the line unambiguously.
"""
try:
results = []
with open(path) as f:
for i, line in enumerate(f):
if re.match(r'\s*{}\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
17 changes: 9 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,36 @@ 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 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 "noentry" does not define entry point function "plugin"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and below the full filename of the plugin would make sense.


[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 "badreturn.plugin" (got None)

[case testInvalidPluginEntryPointReturnValue2]
# flags: --config-file tmp/mypy.ini
Expand All @@ -68,4 +69,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 "badreturn2.plugin" must be a subclass of "mypy.plugin.Plugin"
0