From 1c4f809a0be06416496377fd1bee33379a58638d Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 14 Jul 2018 17:02:59 -0700 Subject: [PATCH 1/5] Allow use of entry-points like strings in mypy.ini to register plugins For example mypkg.mymod:register --- mypy/build.py | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 2b7ec000b8512..a5f5ecc5e942e 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -563,6 +563,7 @@ def load_plugins(options: Options, errors: Errors) -> Plugin: Return a plugin that encapsulates all plugins chained together. Always at least include the default plugin (it's last in the chain). """ + import importlib default_plugin = DefaultPlugin(options) # type: Plugin if not options.config_file: @@ -581,32 +582,41 @@ def plugin_error(message: str) -> None: for plugin_path in options.plugins: # 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 '{}'".format(plugin_path)) - plugin_dir = os.path.dirname(plugin_path) + func_name = 'plugin' + plugin_dir = None # type: Optional[str] fnam = os.path.basename(plugin_path) - if not fnam.endswith('.py'): - plugin_error("Plugin '{}' does not have a .py extension".format(fnam)) - module_name = fnam[:-3] - import importlib - sys.path.insert(0, plugin_dir) + if fnam.endswith('.py'): + if not os.path.isfile(plugin_path): + plugin_error("Can't find plugin '{}'".format(plugin_path)) + plugin_dir = os.path.dirname(plugin_path) + module_name = fnam[:-3] + sys.path.insert(0, plugin_dir) + else: + if ':' in plugin_path: + module_name, func_name = plugin_path.rsplit(':', 1) + else: + module_name = plugin_path + try: - m = importlib.import_module(module_name) + module = importlib.import_module(module_name) except Exception: print('Error importing plugin {}\n'.format(plugin_path)) raise # Propagate to display traceback finally: - 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"'.format( - plugin_path)) + if plugin_dir is not None: + assert sys.path[0] == plugin_dir + del sys.path[0] + + if not hasattr(module, func_name): + plugin_error('Plugin \'{}\' does not define entry point function "%s"'.format( + plugin_path, func_name)) + try: - plugin_type = getattr(m, 'plugin')(__version__) + plugin_type = getattr(module, func_name)(__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} (in {})'.format( From 068aa76b10723eab5547ffba38bad45bc8dbc4aa Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 14 Jul 2018 18:18:27 -0700 Subject: [PATCH 2/5] Fix tests --- mypy/build.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index a5f5ecc5e942e..927f16a90481b 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -585,12 +585,14 @@ def plugin_error(message: str) -> None: func_name = 'plugin' plugin_dir = None # type: Optional[str] fnam = os.path.basename(plugin_path) - if fnam.endswith('.py'): + if plugin_path.endswith('.py'): if not os.path.isfile(plugin_path): plugin_error("Can't find plugin '{}'".format(plugin_path)) plugin_dir = os.path.dirname(plugin_path) module_name = fnam[:-3] sys.path.insert(0, plugin_dir) + elif re.search(r'\/', plugin_path): + plugin_error("Plugin '{}' does not have a .py extension".format(fnam)) else: if ':' in plugin_path: module_name, func_name = plugin_path.rsplit(':', 1) @@ -608,7 +610,7 @@ def plugin_error(message: str) -> None: del sys.path[0] if not hasattr(module, func_name): - plugin_error('Plugin \'{}\' does not define entry point function "%s"'.format( + plugin_error('Plugin \'{}\' does not define entry point function "{}"'.format( plugin_path, func_name)) try: From 63b2e42447904e7ed7802a37fb6bb1bd2a0ea703 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 14 Jul 2018 20:20:40 -0700 Subject: [PATCH 3/5] Allow file paths to specify entry point function --- mypy/build.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 927f16a90481b..060e1748bc8d1 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -581,23 +581,23 @@ def plugin_error(message: str) -> None: errors.set_file(options.config_file, None) for plugin_path in options.plugins: # Plugin paths are relative to the config file location. - plugin_path = os.path.join(os.path.dirname(options.config_file), plugin_path) func_name = 'plugin' plugin_dir = None # type: Optional[str] - fnam = os.path.basename(plugin_path) + if ':' in os.path.basename(plugin_path): + plugin_path, func_name = plugin_path.rsplit(':', 1) if plugin_path.endswith('.py'): + 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 '{}'".format(plugin_path)) plugin_dir = os.path.dirname(plugin_path) + fnam = os.path.basename(plugin_path) module_name = fnam[:-3] sys.path.insert(0, plugin_dir) elif re.search(r'\/', plugin_path): + fnam = os.path.basename(plugin_path) plugin_error("Plugin '{}' does not have a .py extension".format(fnam)) else: - if ':' in plugin_path: - module_name, func_name = plugin_path.rsplit(':', 1) - else: - module_name = plugin_path + module_name = plugin_path try: module = importlib.import_module(module_name) From bfebf3e4d2766e5a6560cacc6569f0fd82b59dae Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 15 Jul 2018 12:39:05 -0700 Subject: [PATCH 4/5] Add tests --- mypy/build.py | 3 +- mypy/test/testcheck.py | 9 ++++- test-data/unit/check-custom-plugin.test | 49 ++++++++++++++++++++++++- test-data/unit/plugins/customentry.py | 14 +++++++ 4 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 test-data/unit/plugins/customentry.py diff --git a/mypy/build.py b/mypy/build.py index 060e1748bc8d1..415d3baa0ff43 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -602,8 +602,7 @@ def plugin_error(message: str) -> None: try: module = importlib.import_module(module_name) except Exception: - print('Error importing plugin {}\n'.format(plugin_path)) - raise # Propagate to display traceback + plugin_error("Error importing plugin '{}'".format(plugin_path)) finally: if plugin_dir is not None: assert sys.path[0] == plugin_dir diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index fd25f34a76e0c..10202fe99eab9 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -8,7 +8,7 @@ from mypy import build from mypy.build import BuildSource, Graph, SearchPaths -from mypy.test.config import test_temp_dir +from mypy.test.config import test_temp_dir, test_data_prefix from mypy.test.data import DataDrivenTestCase, DataSuite, FileOperation, UpdateFile from mypy.test.helpers import ( assert_string_arrays_equal, normalize_error_messages, assert_module_equivalence, @@ -152,6 +152,9 @@ def run_case_once(self, testcase: DataDrivenTestCase, sources.append(BuildSource(program_path, module_name, None if incremental_step else program_text)) + plugin_dir = os.path.join(test_data_prefix, 'plugins') + sys.path.insert(0, plugin_dir) + res = None try: res = build.build(sources=sources, @@ -160,6 +163,10 @@ def run_case_once(self, testcase: DataDrivenTestCase, a = res.errors except CompileError as e: a = e.messages + finally: + assert sys.path[0] == plugin_dir + del sys.path[0] + a = normalize_error_messages(a) # Make sure error messages match diff --git a/test-data/unit/check-custom-plugin.test b/test-data/unit/check-custom-plugin.test index 6ea4e3e5ab332..e2bf3ae4facb9 100644 --- a/test-data/unit/check-custom-plugin.test +++ b/test-data/unit/check-custom-plugin.test @@ -3,7 +3,7 @@ -- Note: Plugins used by tests live under test-data/unit/plugins. Defining -- plugin files in test cases does not work reliably. -[case testFunctionPlugin] +[case testFunctionPluginFile] # flags: --config-file tmp/mypy.ini def f() -> str: ... reveal_type(f()) # E: Revealed type is 'builtins.int' @@ -11,6 +11,14 @@ reveal_type(f()) # E: Revealed type is 'builtins.int' [[mypy] plugins=/test-data/unit/plugins/fnplugin.py +[case testFunctionPlugin] +# flags: --config-file tmp/mypy.ini +def f() -> str: ... +reveal_type(f()) # E: Revealed type is 'builtins.int' +[file mypy.ini] +[[mypy] +plugins=fnplugin + [case testFunctionPluginFullnameIsNotNone] # flags: --config-file tmp/mypy.ini from typing import Callable, TypeVar @@ -35,7 +43,19 @@ reveal_type(h()) # E: Revealed type is 'Any' plugins=/test-data/unit/plugins/fnplugin.py, /test-data/unit/plugins/plugin2.py -[case testMissingPlugin] +[case testTwoPluginsMixedType] +# flags: --config-file tmp/mypy.ini +def f(): ... +def g(): ... +def h(): ... +reveal_type(f()) # E: Revealed type is 'builtins.int' +reveal_type(g()) # E: Revealed type is 'builtins.str' +reveal_type(h()) # E: Revealed type is 'Any' +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/fnplugin.py, plugin2 + +[case testMissingPluginFile] # flags: --config-file tmp/mypy.ini [file mypy.ini] [[mypy] @@ -44,6 +64,15 @@ plugins=missing.py tmp/mypy.ini:2: error: Can't find plugin 'tmp/missing.py' --' (work around syntax highlighting) +[case testMissingPlugin] +# flags: --config-file tmp/mypy.ini +[file mypy.ini] +[[mypy] +plugins=missing +[out] +tmp/mypy.ini:2: error: Error importing plugin 'missing' +--' (work around syntax highlighting) + [case testMultipleSectionsDefinePlugin] # flags: --config-file tmp/mypy.ini [file mypy.ini] @@ -74,6 +103,22 @@ tmp/mypy.ini:2: error: Plugin 'badext.pyi' does not have a .py extension [out] tmp/mypy.ini:2: error: Plugin '/test-data/unit/plugins/noentry.py' does not define entry point function "plugin" +[case testCustomPluginEntryPointFile] +# flags: --config-file tmp/mypy.ini +def f() -> str: ... +reveal_type(f()) # E: Revealed type is 'builtins.int' +[file mypy.ini] +[[mypy] +plugins=/test-data/unit/plugins/customentry.py:register + +[case testCustomPluginEntryPoint] +# flags: --config-file tmp/mypy.ini +def f() -> str: ... +reveal_type(f()) # E: Revealed type is 'builtins.int' +[file mypy.ini] +[[mypy] +plugins=customentry:register + [case testInvalidPluginEntryPointReturnValue] # flags: --config-file tmp/mypy.ini def f(): pass diff --git a/test-data/unit/plugins/customentry.py b/test-data/unit/plugins/customentry.py new file mode 100644 index 0000000000000..f8b86c33dcfc4 --- /dev/null +++ b/test-data/unit/plugins/customentry.py @@ -0,0 +1,14 @@ +from mypy.plugin import Plugin + +class MyPlugin(Plugin): + def get_function_hook(self, fullname): + if fullname == '__main__.f': + return my_hook + assert fullname is not None + return None + +def my_hook(ctx): + return ctx.api.named_generic_type('builtins.int', []) + +def register(version): + return MyPlugin From 27702c3cffd5a4314233db242739a13ae234c75e Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 3 Aug 2018 13:03:41 -0700 Subject: [PATCH 5/5] Address review notes --- mypy/build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 415d3baa0ff43..78ea4ee821c0e 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -580,12 +580,12 @@ def plugin_error(message: str) -> None: custom_plugins = [] # type: List[Plugin] errors.set_file(options.config_file, None) for plugin_path in options.plugins: - # Plugin paths are relative to the config file location. func_name = 'plugin' plugin_dir = None # type: Optional[str] if ':' in os.path.basename(plugin_path): plugin_path, func_name = plugin_path.rsplit(':', 1) if plugin_path.endswith('.py'): + # Plugin paths can be 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 '{}'".format(plugin_path)) @@ -593,7 +593,7 @@ def plugin_error(message: str) -> None: fnam = os.path.basename(plugin_path) module_name = fnam[:-3] sys.path.insert(0, plugin_dir) - elif re.search(r'\/', plugin_path): + elif re.search(r'[\\/]', plugin_path): fnam = os.path.basename(plugin_path) plugin_error("Plugin '{}' does not have a .py extension".format(fnam)) else: