Skip to content
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

fix: fix broken dynamic import of rplugin modules #534

Merged
merged 1 commit into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
46 changes: 24 additions & 22 deletions pynvim/plugin/host.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# type: ignore
"""Implements a Nvim host for python plugins."""

import importlib
import inspect
import logging
import os
import os.path
import re
import sys
from functools import partial
from traceback import format_exc
from typing import Any, Sequence
Expand All @@ -23,25 +26,18 @@
host_method_spec = {"poll": {}, "specs": {"nargs": 1}, "shutdown": {}}


def handle_import(directory, name):
"""Import a python file given a known location.
def _handle_import(path: str, name: str):
"""Import python module `name` from a known file path or module directory.

Currently works on both python2 or 3.
The path should be the base directory from which the module can be imported.
To support python 3.12, the use of `imp` should be avoided.
@see https://docs.python.org/3.12/whatsnew/3.12.html#imp
"""
try: # Python3
from importlib.util import module_from_spec, spec_from_file_location
except ImportError: # Python2.7
import imp
from pynvim.compat import find_module
file, pathname, descr = find_module(name, [directory])
module = imp.load_module(name, file, pathname, descr)
return module
else:
spec = spec_from_file_location(name, location=directory)
if spec is not None:
return module_from_spec(spec)
else:
raise ImportError
if not name:
raise ValueError("Missing module name.")

sys.path.append(path)
return importlib.import_module(name)


class Host(object):
Expand Down Expand Up @@ -167,22 +163,28 @@ def _missing_handler_error(self, name, kind):
return msg

def _load(self, plugins: Sequence[str]) -> None:
"""Load the remote plugins and register handlers defined in the plugins.

Args:
plugins: List of plugin paths to rplugin python modules
registered by remote#host#RegisterPlugin('python3', ...)
(see the generated rplugin.vim manifest)
"""
# self.nvim.err_write("host init _load\n", async_=True)
has_script = False
for path in plugins:
path = os.path.normpath(path) # normalize path
err = None
if path in self._loaded:
error('{} is already loaded'.format(path))
warn('{} is already loaded'.format(path))
continue
try:
if path == "script_host.py":
module = script_host
has_script = True
else:
directory, name = os.path.split(os.path.splitext(path)[0])
try:
module = handle_import(directory, name)
except ImportError:
return
module = _handle_import(directory, name)
handlers = []
self._discover_classes(module, handlers, path)
self._discover_functions(module, handlers, path, False)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""The `mymodule` package for the fixture module plugin."""
# pylint: disable=all

# Somehow the plugin might be using relative imports.
from .plugin import MyPlugin as MyPlugin

# ... or absolute import (assuming this is the root package)
import mymodule.plugin # noqa: I100
assert mymodule.plugin.MyPlugin is MyPlugin
13 changes: 13 additions & 0 deletions test/fixtures/module_plugin/rplugin/python3/mymodule/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Actual implement lies here."""
import pynvim as neovim
import pynvim.api


@neovim.plugin
class MyPlugin:
def __init__(self, nvim: pynvim.api.Nvim):
self.nvim = nvim

@neovim.command("ModuleHelloWorld")
def hello_world(self) -> None:
self.nvim.command("echom 'MyPlugin: Hello World!'")
13 changes: 13 additions & 0 deletions test/fixtures/simple_plugin/rplugin/python3/simple_nvim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import neovim

import pynvim.api


@neovim.plugin
class SimplePlugin:
def __init__(self, nvim: pynvim.api.Nvim):
self.nvim = nvim

@neovim.command("SimpleHelloWorld")
def hello_world(self) -> None:
self.nvim.command("echom 'SimplePlugin: Hello World!'")
25 changes: 24 additions & 1 deletion test/test_host.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# -*- coding: utf-8 -*-
# type: ignore
# pylint: disable=protected-access
import os
from typing import Sequence

from pynvim.plugin.host import Host, host_method_spec
from pynvim.plugin.script_host import ScriptHost

__PATH__ = os.path.abspath(os.path.dirname(__file__))


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


def test_host_import_rplugin_modules(vim):
# Test whether a Host can load and import rplugins (#461).
# See also $VIMRUNTIME/autoload/provider/pythonx.vim.
h = Host(vim)
plugins: Sequence[str] = [ # plugin paths like real rplugins
os.path.join(__PATH__, "./fixtures/simple_plugin/rplugin/python3/simple_nvim.py"),
os.path.join(__PATH__, "./fixtures/module_plugin/rplugin/python3/mymodule/"),
os.path.join(__PATH__, "./fixtures/module_plugin/rplugin/python3/mymodule"), # duplicate
]
h._load(plugins)
assert len(h._loaded) == 2

# pylint: disable-next=unbalanced-tuple-unpacking
simple_nvim, mymodule = list(h._loaded.values())
assert simple_nvim['module'].__name__ == 'simple_nvim'
assert mymodule['module'].__name__ == 'mymodule'


def test_host_clientinfo(vim):
h = Host(vim)
assert h._request_handlers.keys() == host_method_spec.keys()
Expand Down
Loading