Skip to content

Commit 870d1d8

Browse files
authored
Improve plugin-related error messages (#3544)
Address feedback by @gvanrossum in #3517.
1 parent fc974b1 commit 870d1d8

File tree

2 files changed

+65
-19
lines changed

2 files changed

+65
-19
lines changed

mypy/build.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import hashlib
1717
import json
1818
import os.path
19+
import re
1920
import sys
2021
import time
2122
from os.path import dirname, basename
@@ -343,23 +344,29 @@ def load_custom_plugins(default_plugin: Plugin, options: Options, errors: Errors
343344
back to default_plugin.
344345
"""
345346

347+
if not options.config_file:
348+
return default_plugin
349+
350+
line = find_config_file_line_number(options.config_file, 'mypy', 'plugins')
351+
if line == -1:
352+
line = 1 # We need to pick some line number that doesn't look too confusing
353+
346354
def plugin_error(message: str) -> None:
347-
errors.report(0, 0, message)
355+
errors.report(line, 0, message)
348356
errors.raise_error()
349357

358+
errors.set_file(options.config_file, None)
350359
custom_plugins = []
351360
for plugin_path in options.plugins:
352-
if options.config_file:
353-
# Plugin paths are relative to the config file location.
354-
plugin_path = os.path.join(os.path.dirname(options.config_file), plugin_path)
355-
errors.set_file(plugin_path, None)
361+
# Plugin paths are relative to the config file location.
362+
plugin_path = os.path.join(os.path.dirname(options.config_file), plugin_path)
356363

357364
if not os.path.isfile(plugin_path):
358-
plugin_error("Can't find plugin")
365+
plugin_error("Can't find plugin '{}'".format(plugin_path))
359366
plugin_dir = os.path.dirname(plugin_path)
360367
fnam = os.path.basename(plugin_path)
361368
if not fnam.endswith('.py'):
362-
plugin_error("Plugin must have .py extension")
369+
plugin_error("Plugin '{}' does not have a .py extension".format(fnam))
363370
module_name = fnam[:-3]
364371
import importlib
365372
sys.path.insert(0, plugin_dir)
@@ -372,19 +379,21 @@ def plugin_error(message: str) -> None:
372379
assert sys.path[0] == plugin_dir
373380
del sys.path[0]
374381
if not hasattr(m, 'plugin'):
375-
plugin_error('Plugin does not define entry point function "plugin"')
382+
plugin_error('Plugin \'{}\' does not define entry point function "plugin"'.format(
383+
plugin_path))
376384
try:
377385
plugin_type = getattr(m, 'plugin')(__version__)
378386
except Exception:
379387
print('Error calling the plugin(version) entry point of {}\n'.format(plugin_path))
380388
raise # Propagate to display traceback
381389
if not isinstance(plugin_type, type):
382390
plugin_error(
383-
'Type object expected as the return value of "plugin" (got {!r})'.format(
384-
plugin_type))
391+
'Type object expected as the return value of "plugin"; got {!r} (in {})'.format(
392+
plugin_type, plugin_path))
385393
if not issubclass(plugin_type, Plugin):
386394
plugin_error(
387-
'Return value of "plugin" must be a subclass of "mypy.plugin.Plugin"')
395+
'Return value of "plugin" must be a subclass of "mypy.plugin.Plugin" '
396+
'(in {})'.format(plugin_path))
388397
try:
389398
custom_plugins.append(plugin_type(options.python_version))
390399
except Exception:
@@ -397,6 +406,29 @@ def plugin_error(message: str) -> None:
397406
return ChainedPlugin(options.python_version, custom_plugins + [default_plugin])
398407

399408

409+
def find_config_file_line_number(path: str, section: str, setting_name: str) -> int:
410+
"""Return the approximate location of setting_name within mypy config file.
411+
412+
Return -1 if can't determine the line unambiguously.
413+
"""
414+
in_desired_section = False
415+
try:
416+
results = []
417+
with open(path) as f:
418+
for i, line in enumerate(f):
419+
line = line.strip()
420+
if line.startswith('[') and line.endswith(']'):
421+
current_section = line[1:-1].strip()
422+
in_desired_section = (current_section == section)
423+
elif in_desired_section and re.match(r'{}\s*='.format(setting_name), line):
424+
results.append(i + 1)
425+
if len(results) == 1:
426+
return results[0]
427+
except OSError:
428+
pass
429+
return -1
430+
431+
400432
# TODO: Get rid of all_types. It's not used except for one log message.
401433
# Maybe we could instead publish a map from module ID to its type_map.
402434
class BuildManager:

test-data/unit/check-custom-plugin.test

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,35 +30,49 @@ plugins=<ROOT>/test-data/unit/plugins/fnplugin.py,
3030
[[mypy]
3131
plugins=missing.py
3232
[out]
33-
tmp/missing.py:0: error: Can't find plugin
33+
tmp/mypy.ini:2: error: Can't find plugin 'tmp/missing.py'
34+
--' (work around syntax highlighting)
35+
36+
[case testMultipleSectionsDefinePlugin]
37+
# flags: --config-file tmp/mypy.ini
38+
[file mypy.ini]
39+
[[acme]
40+
plugins=acmeplugin
41+
[[mypy]
42+
plugins=missing.py
43+
[[another]
44+
plugins=another_plugin
45+
[out]
46+
tmp/mypy.ini:4: error: Can't find plugin 'tmp/missing.py'
3447
--' (work around syntax highlighting)
3548

3649
[case testInvalidPluginExtension]
3750
# flags: --config-file tmp/mypy.ini
3851
[file mypy.ini]
3952
[[mypy]
40-
plugins=badext.pyi
41-
[file badext.pyi]
53+
plugins=dir/badext.pyi
54+
[file dir/badext.pyi]
4255
[out]
43-
tmp/badext.pyi:0: error: Plugin must have .py extension
56+
tmp/mypy.ini:2: error: Plugin 'badext.pyi' does not have a .py extension
4457

4558
[case testMissingPluginEntryPoint]
4659
# flags: --config-file tmp/mypy.ini
4760
[file mypy.ini]
4861
[[mypy]
49-
plugins=<ROOT>/test-data/unit/plugins/noentry.py
62+
plugins = <ROOT>/test-data/unit/plugins/noentry.py
5063
[out]
51-
<ROOT>/test-data/unit/plugins/noentry.py:0: error: Plugin does not define entry point function "plugin"
64+
tmp/mypy.ini:2: error: Plugin '<ROOT>/test-data/unit/plugins/noentry.py' does not define entry point function "plugin"
5265

5366
[case testInvalidPluginEntryPointReturnValue]
5467
# flags: --config-file tmp/mypy.ini
5568
def f(): pass
5669
f()
5770
[file mypy.ini]
5871
[[mypy]
72+
5973
plugins=<ROOT>/test-data/unit/plugins/badreturn.py
6074
[out]
61-
<ROOT>/test-data/unit/plugins/badreturn.py:0: error: Type object expected as the return value of "plugin" (got None)
75+
tmp/mypy.ini:3: error: Type object expected as the return value of "plugin"; got None (in <ROOT>/test-data/unit/plugins/badreturn.py)
6276

6377
[case testInvalidPluginEntryPointReturnValue2]
6478
# flags: --config-file tmp/mypy.ini
@@ -68,4 +82,4 @@ f()
6882
[[mypy]
6983
plugins=<ROOT>/test-data/unit/plugins/badreturn2.py
7084
[out]
71-
<ROOT>/test-data/unit/plugins/badreturn2.py:0: error: Return value of "plugin" must be a subclass of "mypy.plugin.Plugin"
85+
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 commit comments

Comments
 (0)