diff --git a/mypy/build.py b/mypy/build.py index e4b202c4e72b..4998de54f411 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -16,6 +16,7 @@ import hashlib import json import os.path +import re import sys import time from os.path import dirname, basename @@ -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) @@ -372,7 +379,8 @@ 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: @@ -380,11 +388,12 @@ def plugin_error(message: str) -> None: 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: @@ -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: diff --git a/test-data/unit/check-custom-plugin.test b/test-data/unit/check-custom-plugin.test index 30b00a4b3a62..69d469a6f415 100644 --- a/test-data/unit/check-custom-plugin.test +++ b/test-data/unit/check-custom-plugin.test @@ -30,25 +30,38 @@ plugins=/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=/test-data/unit/plugins/noentry.py + plugins = /test-data/unit/plugins/noentry.py [out] -/test-data/unit/plugins/noentry.py:0: error: Plugin does not define entry point function "plugin" +tmp/mypy.ini:2: error: Plugin '/test-data/unit/plugins/noentry.py' does not define entry point function "plugin" [case testInvalidPluginEntryPointReturnValue] # flags: --config-file tmp/mypy.ini @@ -56,9 +69,10 @@ def f(): pass f() [file mypy.ini] [[mypy] + plugins=/test-data/unit/plugins/badreturn.py [out] -/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 /test-data/unit/plugins/badreturn.py) [case testInvalidPluginEntryPointReturnValue2] # flags: --config-file tmp/mypy.ini @@ -68,4 +82,4 @@ f() [[mypy] plugins=/test-data/unit/plugins/badreturn2.py [out] -/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 /test-data/unit/plugins/badreturn2.py)