Skip to content

Commit f244597

Browse files
authored
fix: broken dynamic import of rplugin modules #534
The removal of `imp` package (#461) in order to supprot Python 3.12 had a bug where rplugins can't be loaded and the ImportError exception was silenced, making the remote provider throwing lots of errors. This commit fixes broken dynamic import of python modules from the registered rplugins. We add tests for Host._load, with loading rplugins consisting of: (1) single-file module (e.g., `rplugins/simple_plugin.py`) (2) package (e.g., `rplugins/mymodule/__init__.py`)
1 parent 798dfc3 commit f244597

File tree

5 files changed

+83
-23
lines changed

5 files changed

+83
-23
lines changed

pynvim/plugin/host.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# type: ignore
22
"""Implements a Nvim host for python plugins."""
3+
4+
import importlib
35
import inspect
46
import logging
57
import os
68
import os.path
79
import re
10+
import sys
811
from functools import partial
912
from traceback import format_exc
1013
from typing import Any, Sequence
@@ -23,25 +26,18 @@
2326
host_method_spec = {"poll": {}, "specs": {"nargs": 1}, "shutdown": {}}
2427

2528

26-
def handle_import(directory, name):
27-
"""Import a python file given a known location.
29+
def _handle_import(path: str, name: str):
30+
"""Import python module `name` from a known file path or module directory.
2831
29-
Currently works on both python2 or 3.
32+
The path should be the base directory from which the module can be imported.
33+
To support python 3.12, the use of `imp` should be avoided.
34+
@see https://docs.python.org/3.12/whatsnew/3.12.html#imp
3035
"""
31-
try: # Python3
32-
from importlib.util import module_from_spec, spec_from_file_location
33-
except ImportError: # Python2.7
34-
import imp
35-
from pynvim.compat import find_module
36-
file, pathname, descr = find_module(name, [directory])
37-
module = imp.load_module(name, file, pathname, descr)
38-
return module
39-
else:
40-
spec = spec_from_file_location(name, location=directory)
41-
if spec is not None:
42-
return module_from_spec(spec)
43-
else:
44-
raise ImportError
36+
if not name:
37+
raise ValueError("Missing module name.")
38+
39+
sys.path.append(path)
40+
return importlib.import_module(name)
4541

4642

4743
class Host(object):
@@ -167,22 +163,28 @@ def _missing_handler_error(self, name, kind):
167163
return msg
168164

169165
def _load(self, plugins: Sequence[str]) -> None:
166+
"""Load the remote plugins and register handlers defined in the plugins.
167+
168+
Args:
169+
plugins: List of plugin paths to rplugin python modules
170+
registered by remote#host#RegisterPlugin('python3', ...)
171+
(see the generated rplugin.vim manifest)
172+
"""
173+
# self.nvim.err_write("host init _load\n", async_=True)
170174
has_script = False
171175
for path in plugins:
176+
path = os.path.normpath(path) # normalize path
172177
err = None
173178
if path in self._loaded:
174-
error('{} is already loaded'.format(path))
179+
warn('{} is already loaded'.format(path))
175180
continue
176181
try:
177182
if path == "script_host.py":
178183
module = script_host
179184
has_script = True
180185
else:
181186
directory, name = os.path.split(os.path.splitext(path)[0])
182-
try:
183-
module = handle_import(directory, name)
184-
except ImportError:
185-
return
187+
module = _handle_import(directory, name)
186188
handlers = []
187189
self._discover_classes(module, handlers, path)
188190
self._discover_functions(module, handlers, path, False)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""The `mymodule` package for the fixture module plugin."""
2+
# pylint: disable=all
3+
4+
# Somehow the plugin might be using relative imports.
5+
from .plugin import MyPlugin as MyPlugin
6+
7+
# ... or absolute import (assuming this is the root package)
8+
import mymodule.plugin # noqa: I100
9+
assert mymodule.plugin.MyPlugin is MyPlugin
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Actual implement lies here."""
2+
import pynvim as neovim
3+
import pynvim.api
4+
5+
6+
@neovim.plugin
7+
class MyPlugin:
8+
def __init__(self, nvim: pynvim.api.Nvim):
9+
self.nvim = nvim
10+
11+
@neovim.command("ModuleHelloWorld")
12+
def hello_world(self) -> None:
13+
self.nvim.command("echom 'MyPlugin: Hello World!'")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import neovim
2+
3+
import pynvim.api
4+
5+
6+
@neovim.plugin
7+
class SimplePlugin:
8+
def __init__(self, nvim: pynvim.api.Nvim):
9+
self.nvim = nvim
10+
11+
@neovim.command("SimpleHelloWorld")
12+
def hello_world(self) -> None:
13+
self.nvim.command("echom 'SimplePlugin: Hello World!'")

test/test_host.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
# -*- coding: utf-8 -*-
21
# type: ignore
2+
# pylint: disable=protected-access
3+
import os
4+
from typing import Sequence
5+
36
from pynvim.plugin.host import Host, host_method_spec
47
from pynvim.plugin.script_host import ScriptHost
58

9+
__PATH__ = os.path.abspath(os.path.dirname(__file__))
10+
611

712
def test_host_imports(vim):
813
h = ScriptHost(vim)
@@ -11,6 +16,24 @@ def test_host_imports(vim):
1116
assert h.module.__dict__['sys']
1217

1318

19+
def test_host_import_rplugin_modules(vim):
20+
# Test whether a Host can load and import rplugins (#461).
21+
# See also $VIMRUNTIME/autoload/provider/pythonx.vim.
22+
h = Host(vim)
23+
plugins: Sequence[str] = [ # plugin paths like real rplugins
24+
os.path.join(__PATH__, "./fixtures/simple_plugin/rplugin/python3/simple_nvim.py"),
25+
os.path.join(__PATH__, "./fixtures/module_plugin/rplugin/python3/mymodule/"),
26+
os.path.join(__PATH__, "./fixtures/module_plugin/rplugin/python3/mymodule"), # duplicate
27+
]
28+
h._load(plugins)
29+
assert len(h._loaded) == 2
30+
31+
# pylint: disable-next=unbalanced-tuple-unpacking
32+
simple_nvim, mymodule = list(h._loaded.values())
33+
assert simple_nvim['module'].__name__ == 'simple_nvim'
34+
assert mymodule['module'].__name__ == 'mymodule'
35+
36+
1437
def test_host_clientinfo(vim):
1538
h = Host(vim)
1639
assert h._request_handlers.keys() == host_method_spec.keys()

0 commit comments

Comments
 (0)