Skip to content

Commit 1f0b4f2

Browse files
committed
Refactor to conform with Nvim external plugin infrastructure
- Move some code from plugin_host.py to script_host.py - Remove script_host.py and tests, it now belongs in Neovim repository - Rename "plugins" to "plugin" - Add plugin/decorators.py module. Now plugins are defined with these decorators. - Rename plugins/plugin_host.py to plugin/host.py - Fix flake8 errors in plugin/host.py
1 parent 5b5a5d4 commit 1f0b4f2

File tree

9 files changed

+323
-453
lines changed

9 files changed

+323
-453
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ script:
3838
./scripts/run-api-tests.exp "nosetests" "nvim -u NONE";
3939
NVIM_SPAWN_ARGV='["nvim", "-u", "NONE", "--embed"]' nosetests;
4040
else
41-
flake8 --exclude ./neovim/plugins neovim;
41+
flake8 neovim;
4242
fi
4343
after_script:
4444
- if [ $CI_TARGET = tests ]; then

neovim/__init__.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
"""
55
import logging
66
import os
7+
import sys
78

89
from .api import DecodeHook, Nvim, SessionHook
910
from .msgpack_rpc import (socket_session, spawn_session, stdio_session,
1011
tcp_session)
11-
from .plugins import PluginHost, ScriptHost
12+
from .plugin import (Host, autocmd, command, encoding, function, plugin,
13+
rpc_export, shutdown_hook)
1214

1315

1416
__all__ = ('tcp_session', 'socket_session', 'stdio_session', 'spawn_session',
15-
'start_host', 'DecodeHook', 'Nvim', 'SessionHook')
17+
'start_host', 'autocmd', 'command', 'encoding', 'function',
18+
'plugin', 'rpc_export', 'Host', 'DecodeHook', 'Nvim',
19+
'SessionHook', 'shutdown_hook')
1620

1721

1822
def start_host(session=None):
@@ -30,6 +34,15 @@ def start_host(session=None):
3034
defined as a separate executable. It is exposed as a library function for
3135
testing purposes only.
3236
"""
37+
plugins = []
38+
for arg in sys.argv:
39+
_, ext = os.path.splitext(arg)
40+
if ext == '.py':
41+
plugins.append(arg)
42+
43+
if not plugins:
44+
sys.exit('must specify at least one plugin as argument')
45+
3346
logger = logging.getLogger(__name__)
3447
if 'NVIM_PYTHON_LOG_FILE' in os.environ:
3548
logfile = os.environ['NVIM_PYTHON_LOG_FILE'].strip()
@@ -48,9 +61,8 @@ def start_host(session=None):
4861
logger.setLevel(level)
4962
if not session:
5063
session = stdio_session()
51-
nvim = Nvim.from_session(session)
52-
with PluginHost(nvim, preloaded=[ScriptHost]) as host:
53-
host.run()
64+
host = Host(Nvim.from_session(session))
65+
host.start(plugins)
5466

5567

5668
# Required for python 2.6

neovim/plugin/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Nvim plugin/host subpackage."""
2+
3+
from .decorators import (autocmd, command, encoding, function, plugin,
4+
rpc_export, shutdown_hook)
5+
from .host import Host
6+
7+
8+
__all__ = ('Host', 'plugin', 'rpc_export', 'command', 'autocmd',
9+
'function', 'encoding', 'shutdown_hook')

neovim/plugin/decorators.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""Decorators used by python host plugin system."""
2+
import inspect
3+
4+
5+
import logging
6+
logger = logging.getLogger(__name__)
7+
debug, info, warn = (logger.debug, logger.info, logger.warn,)
8+
__all__ = ('plugin', 'rpc_export', 'command', 'autocmd', 'function',
9+
'encoding', 'shutdown_hook')
10+
11+
12+
def plugin(cls):
13+
"""Tag a class as a plugin.
14+
15+
This decorator is required to make the a class methods discoverable by the
16+
plugin_load method of the host.
17+
"""
18+
cls._nvim_plugin = True
19+
# the _nvim_bind attribute is set to True by default, meaning that
20+
# decorated functions have a bound Nvim instance as first argument.
21+
# For methods in a plugin-decorated class this is not required, because
22+
# the class initializer will already receive the nvim object.
23+
for _, fn in inspect.getmembers(cls, inspect.ismethod):
24+
if hasattr(fn, '_nvim_bind'):
25+
fn.im_func._nvim_bind = False
26+
return cls
27+
28+
29+
def rpc_export(rpc_method_name, sync=False):
30+
"""Export a function or plugin method as a msgpack-rpc request handler."""
31+
def dec(f):
32+
f._nvim_rpc_method_name = rpc_method_name
33+
f._nvim_rpc_sync = sync
34+
f._nvim_bind = True
35+
f._nvim_prefix_plugin_path = False
36+
return f
37+
return dec
38+
39+
40+
def command(name, nargs=0, complete=None, range=None, count=None, bang=False,
41+
register=False, sync=False, eval=None):
42+
"""Tag a function or plugin method as a Nvim command handler."""
43+
def dec(f):
44+
f._nvim_rpc_method_name = 'command:{0}'.format(name)
45+
f._nvim_rpc_sync = sync
46+
f._nvim_bind = True
47+
f._nvim_prefix_plugin_path = True
48+
49+
opts = {}
50+
51+
if range:
52+
opts['range'] = '' if range is True else str(range)
53+
elif count:
54+
opts['count'] = count
55+
56+
if bang:
57+
opts['bang'] = True
58+
59+
if register:
60+
opts['register'] = True
61+
62+
if nargs:
63+
opts['nargs'] = nargs
64+
65+
if complete:
66+
opts['complete'] = complete
67+
68+
if eval:
69+
opts['eval'] = eval
70+
71+
f.nvim_rpc_spec = {
72+
'type': 'command',
73+
'name': name,
74+
'sync': sync,
75+
'opts': opts
76+
}
77+
return f
78+
return dec
79+
80+
81+
def autocmd(name, pattern=None, sync=False, eval=None):
82+
"""Tag a function or plugin method as a Nvim autocommand handler."""
83+
def dec(f):
84+
f._nvim_rpc_method_name = 'autocmd:{0}:{1}'.format(name, pattern)
85+
f._nvim_rpc_sync = sync
86+
f._nvim_bind = True
87+
f._nvim_prefix_plugin_path = True
88+
89+
opts = {
90+
'pattern': pattern or '*'
91+
}
92+
93+
if eval:
94+
opts['eval'] = eval
95+
96+
f.nvim_rpc_spec = {
97+
'type': 'autocmd',
98+
'name': name,
99+
'sync': sync,
100+
'opts': opts
101+
}
102+
return f
103+
return dec
104+
105+
106+
def function(name, range=False, sync=False, eval=None):
107+
"""Tag a function or plugin method as a Nvim function handler."""
108+
def dec(f):
109+
f._nvim_rpc_method_name = 'function:{0}'.format(name)
110+
f._nvim_rpc_sync = sync
111+
f._nvim_bind = True
112+
f._nvim_prefix_plugin_path = True
113+
114+
opts = {}
115+
116+
if range:
117+
opts['range'] = '' if range is True else str(range)
118+
119+
if eval:
120+
opts['eval'] = eval
121+
122+
f.nvim_rpc_spec = {
123+
'type': 'function',
124+
'name': name,
125+
'sync': sync,
126+
'opts': opts
127+
}
128+
return f
129+
return dec
130+
131+
132+
def shutdown_hook(f):
133+
"""Tag a function or method as a shutdown hook."""
134+
f._nvim_shutdown_hook = True
135+
f._nvim_bind = True
136+
return f
137+
138+
139+
def encoding(encoding=True):
140+
"""Configure automatic encoding/decoding of strings."""
141+
def dec(f):
142+
f._nvim_encoding = encoding
143+
return f
144+
return dec

neovim/plugin/host.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""Implements a Nvim host for python plugins."""
2+
import functools
3+
import imp
4+
import inspect
5+
import logging
6+
import os
7+
import os.path
8+
9+
from ..api import DecodeHook
10+
from ..compat import IS_PYTHON3, find_module
11+
12+
13+
__all__ = ('Host')
14+
15+
logger = logging.getLogger(__name__)
16+
debug, info, warn = (logger.debug, logger.info, logger.warn,)
17+
18+
19+
class Host(object):
20+
21+
"""Nvim host for python plugins.
22+
23+
Takes care of loading/unloading plugins and routing msgpack-rpc
24+
requests/notifications to the appropriate handlers.
25+
"""
26+
27+
def __init__(self, nvim):
28+
"""Set handlers for plugin_load/plugin_unload."""
29+
self.nvim = nvim
30+
self._specs = {}
31+
self._loaded = {}
32+
self._notification_handlers = {}
33+
self._request_handlers = {
34+
'poll': lambda: 'ok',
35+
'specs': lambda path: self._specs[path],
36+
'shutdown': self.shutdown
37+
}
38+
self._nvim_encoding = nvim.options['encoding']
39+
40+
def start(self, plugins):
41+
"""Start listening for msgpack-rpc requests and notifications."""
42+
self.nvim.session.run(self._on_request,
43+
self._on_notification,
44+
lambda: self._load(plugins))
45+
46+
def shutdown(self):
47+
"""Shutdown the host."""
48+
self._unload()
49+
self.nvim.session.stop()
50+
51+
def _on_request(self, name, args):
52+
"""Handle a msgpack-rpc request."""
53+
handler = self._request_handlers.get(name, None)
54+
if not handler:
55+
msg = 'no request handler registered for "%s"' % name
56+
warn(msg)
57+
raise Exception(msg)
58+
59+
debug('calling request handler for "%s", args: "%s"', name, args)
60+
rv = handler(*args)
61+
debug("request handler for '%s %s' returns: %s", name, args, rv)
62+
return rv
63+
64+
def _on_notification(self, name, args):
65+
"""Handle a msgpack-rpc notification."""
66+
handler = self._notification_handlers.get(name, None)
67+
if not handler:
68+
warn('no notification handler registered for "%s"', name)
69+
return
70+
71+
debug('calling notification handler for "%s", args: "%s"', name, args)
72+
handler(*args)
73+
74+
def _load(self, plugins):
75+
for path in plugins:
76+
if path in self._loaded:
77+
raise Exception('{0} is already loaded'.format(path))
78+
directory, name = os.path.split(os.path.splitext(path)[0])
79+
file, pathname, description = find_module(name, [directory])
80+
module = imp.load_module(name, file, pathname, description)
81+
handlers = []
82+
self._discover_classes(module, handlers, path)
83+
self._discover_functions(module, handlers, path)
84+
if not handlers:
85+
raise Exception('{0} exports no handlers'.format(path))
86+
self._loaded[path] = {'handlers': handlers, 'module': module}
87+
88+
def _unload(self):
89+
for path, plugin in self._loaded.items():
90+
handlers = plugin['handlers']
91+
for handler in handlers:
92+
method_name = handler._nvim_rpc_method_name
93+
if hasattr(handler, '_nvim_shutdown_hook'):
94+
handler()
95+
elif handler._nvim_rpc_sync:
96+
del self._request_handlers[method_name]
97+
else:
98+
del self._notification_handlers[method_name]
99+
self._specs = {}
100+
self._loaded = {}
101+
102+
def _discover_classes(self, module, handlers, plugin_path):
103+
for _, cls in inspect.getmembers(module, inspect.isclass):
104+
if getattr(cls, '_nvim_plugin', False):
105+
# create an instance of the plugin and pass the nvim object
106+
plugin = cls(self._configure_nvim_for(cls))
107+
# discover handlers in the plugin instance
108+
self._discover_functions(plugin, handlers, plugin_path)
109+
110+
def _discover_functions(self, obj, handlers, plugin_path):
111+
predicate = lambda o: hasattr(o, '_nvim_rpc_method_name')
112+
specs = []
113+
for _, fn in inspect.getmembers(obj, predicate):
114+
if fn._nvim_bind:
115+
# bind a nvim instance to the handler
116+
fn2 = functools.partial(fn, self._configure_nvim_for(fn))
117+
# copy _nvim_* attributes from the original function
118+
for attr in dir(fn):
119+
if attr.startswith('_nvim_'):
120+
setattr(fn2, attr, getattr(fn, attr))
121+
fn = fn2
122+
# register in the rpc handler dict
123+
method = fn._nvim_rpc_method_name
124+
if fn._nvim_prefix_plugin_path:
125+
method = '{0}:{1}'.format(plugin_path, method)
126+
if fn._nvim_rpc_sync:
127+
if method in self._request_handlers:
128+
raise Exception('Request handler for "{0}" is ' +
129+
'already registered'.format(method))
130+
self._request_handlers[method] = fn
131+
else:
132+
if method in self._notification_handlers:
133+
raise Exception('Notification handler for "{0}" is ' +
134+
'already registered'.format(method))
135+
self._notification_handlers[method] = fn
136+
if hasattr(fn, 'nvim_rpc_spec'):
137+
specs.append(fn.nvim_rpc_spec)
138+
handlers.append(fn)
139+
if specs:
140+
self._specs[plugin_path] = specs
141+
142+
def _configure_nvim_for(self, obj):
143+
# Configure a nvim instance for obj(checks encoding configuration)
144+
nvim = self.nvim
145+
encoding = getattr(obj, '_nvim_encoding', None)
146+
if IS_PYTHON3 and encoding is None:
147+
encoding = True
148+
if encoding is True:
149+
encoding = self._nvim_encoding
150+
if encoding:
151+
nvim = nvim.with_hook(DecodeHook(encoding))
152+
return nvim

neovim/plugins/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)