1
|
|
|
"""Implements a Nvim host for python plugins.""" |
2
|
5 |
|
import imp |
3
|
5 |
|
import inspect |
4
|
5 |
|
import logging |
5
|
5 |
|
import os |
6
|
5 |
|
import os.path |
7
|
5 |
|
import re |
8
|
5 |
|
|
9
|
|
|
from collections import deque |
10
|
5 |
|
from functools import partial |
11
|
|
|
from traceback import format_exc |
12
|
5 |
|
|
13
|
5 |
|
from . import script_host |
14
|
5 |
|
from ..api import decode_if_bytes, walk |
15
|
5 |
|
from ..compat import IS_PYTHON3, find_module |
16
|
5 |
|
from ..msgpack_rpc import ErrorResponse |
17
|
|
|
from ..util import format_exc_skip |
18
|
5 |
|
|
19
|
|
|
__all__ = ('Host') |
20
|
5 |
|
|
21
|
5 |
|
logger = logging.getLogger(__name__) |
22
|
|
|
error, debug, info, warn = (logger.error, logger.debug, logger.info, |
23
|
|
|
logger.warning,) |
24
|
|
|
|
25
|
5 |
|
|
26
|
|
|
class Host(object): |
27
|
|
|
|
28
|
|
|
"""Nvim host for python plugins. |
29
|
|
|
|
30
|
|
|
Takes care of loading/unloading plugins and routing msgpack-rpc |
31
|
|
|
requests/notifications to the appropriate handlers. |
32
|
|
|
""" |
33
|
5 |
|
|
34
|
|
|
def __init__(self, nvim): |
35
|
|
|
"""Set handlers for plugin_load/plugin_unload.""" |
36
|
|
|
self.nvim = nvim |
37
|
|
|
self._specs = {} |
38
|
|
|
self._loaded = {} |
39
|
|
|
self._load_errors = {} |
40
|
|
|
self._notification_handlers = {} |
41
|
|
|
self._request_handlers = { |
42
|
|
|
'poll': lambda: 'ok', |
43
|
|
|
'specs': self._on_specs_request, |
44
|
|
|
'shutdown': self.shutdown |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
self.nested_handlers = 0 |
48
|
|
|
self.queued_events = deque() |
49
|
5 |
|
|
50
|
|
|
# Decode per default for Python3 |
51
|
|
|
self._decode_default = IS_PYTHON3 |
52
|
5 |
|
|
53
|
|
|
def _on_async_err(self, msg): |
54
|
|
|
self.nvim.err_write(msg, async=True) |
55
|
|
|
|
56
|
|
|
def start(self, plugins): |
57
|
|
|
"""Start listening for msgpack-rpc requests and notifications.""" |
58
|
|
|
self.nvim.run_loop(self._on_request, |
59
|
5 |
|
self._on_notification, |
60
|
|
|
lambda: self._load(plugins), |
61
|
|
|
err_cb=self._on_async_err) |
62
|
|
|
|
63
|
|
|
def shutdown(self): |
64
|
5 |
|
"""Shutdown the host.""" |
65
|
|
|
self._unload() |
66
|
|
|
self.nvim.stop_loop() |
67
|
|
|
|
68
|
|
|
def _wrap_function(self, fn, sync, decode, nvim_bind, name, *args): |
69
|
|
|
if decode: |
70
|
|
|
args = walk(decode_if_bytes, args, decode) |
71
|
|
|
if nvim_bind is not None: |
72
|
|
|
args.insert(0, nvim_bind) |
73
|
|
|
try: |
74
|
|
|
return fn(*args) |
75
|
|
|
except Exception: |
|
|
|
|
76
|
|
|
if sync: |
77
|
|
|
msg = ("error caught in request handler '{} {}':\n{}" |
78
|
|
|
.format(name, args, format_exc_skip(1))) |
79
|
|
|
raise ErrorResponse(msg) |
80
|
|
|
else: |
81
|
5 |
|
msg = ("error caught in async handler '{} {}'\n{}\n" |
82
|
|
|
.format(name, args, format_exc_skip(1))) |
83
|
|
|
self._on_async_err(msg + "\n") |
84
|
|
|
|
85
|
|
|
def _on_request(self, name, args): |
86
|
|
|
"""Handle a msgpack-rpc request.""" |
87
|
|
|
if IS_PYTHON3: |
88
|
|
|
name = decode_if_bytes(name) |
89
|
|
|
handler = self._request_handlers.get(name, None) |
90
|
|
|
if not handler: |
91
|
|
|
msg = self._missing_handler_error(name, 'request') |
92
|
|
|
error(msg) |
93
|
|
|
raise ErrorResponse(msg) |
94
|
|
|
|
95
|
|
|
debug('calling request handler for "%s", args: "%s"', name, args) |
96
|
5 |
|
try: |
97
|
|
|
self.nested_handlers += 1 |
98
|
|
|
rv = handler(*args) |
99
|
|
|
finally: |
100
|
|
|
self.nested_handlers -= 1 |
101
|
|
|
self._check_queued() |
102
|
|
|
debug("request handler for '%s %s' returns: %s", name, args, rv) |
103
|
|
|
return rv |
104
|
|
|
|
105
|
|
|
def _on_notification(self, name, args): |
106
|
|
|
"""Handle a msgpack-rpc notification.""" |
107
|
|
|
if IS_PYTHON3: |
108
|
|
|
name = decode_if_bytes(name) |
109
|
|
|
handler = self._notification_handlers.get(name, None) |
110
|
5 |
|
if not handler: |
111
|
|
|
msg = self._missing_handler_error(name, 'notification') |
112
|
|
|
error(msg) |
113
|
|
|
self._on_async_err(msg + "\n") |
114
|
|
|
return |
115
|
|
|
|
116
|
|
|
if getattr(handler, "_nvim_rpc_urgent", False) or self.nested_handlers == 0: |
117
|
|
|
debug('calling notification handler for "%s", args: "%s"', name, args) |
118
|
|
|
try: |
119
|
5 |
|
self.nested_handlers += 1 |
120
|
|
|
handler(*args) |
121
|
|
|
finally: |
122
|
|
|
self.nested_handlers -= 1 |
123
|
|
|
self._check_queued() |
124
|
|
|
else: |
125
|
|
|
debug('queuing notification handler for "%s", args: "%s"', name, args) |
126
|
|
|
self.queued_events.append(partial(handler, *args)) |
127
|
|
|
|
128
|
|
|
def _check_queued(self): |
129
|
|
|
if self.nested_handlers > 0: |
130
|
|
|
return |
131
|
|
|
|
132
|
|
|
try: |
133
|
|
|
self.nested_handlers += 1 |
134
|
|
|
while self.queued_events: |
135
|
|
|
ev = self.queued_events.popleft() |
136
|
|
|
try: |
137
|
|
|
ev() |
138
|
|
|
except Exception as e: |
|
|
|
|
139
|
|
|
pass |
140
|
|
|
finally: |
141
|
|
|
self.nested_handlers -= 1 |
142
|
|
|
|
143
|
|
|
def _missing_handler_error(self, name, kind): |
144
|
|
|
msg = 'no {} handler registered for "{}"'.format(kind, name) |
145
|
5 |
|
pathmatch = re.match(r'(.+):[^:]+:[^:]+', name) |
146
|
|
|
if pathmatch: |
147
|
|
|
loader_error = self._load_errors.get(pathmatch.group(1)) |
148
|
|
|
if loader_error is not None: |
149
|
|
|
msg = msg + "\n" + loader_error |
150
|
|
|
return msg |
151
|
|
|
|
152
|
|
|
def _load(self, plugins): |
153
|
|
|
for path in plugins: |
154
|
|
|
err = None |
155
|
|
|
if path in self._loaded: |
156
|
|
|
error('{} is already loaded'.format(path)) |
157
|
|
|
continue |
158
|
|
|
try: |
159
|
5 |
|
if path == "script_host.py": |
160
|
|
|
module = script_host |
161
|
|
|
else: |
162
|
|
|
directory, name = os.path.split(os.path.splitext(path)[0]) |
163
|
|
|
file, pathname, descr = find_module(name, [directory]) |
164
|
|
|
module = imp.load_module(name, file, pathname, descr) |
165
|
|
|
handlers = [] |
166
|
|
|
self._discover_classes(module, handlers, path) |
167
|
5 |
|
self._discover_functions(module, handlers, path) |
168
|
|
|
if not handlers: |
169
|
|
|
error('{} exports no handlers'.format(path)) |
170
|
|
|
continue |
171
|
|
|
self._loaded[path] = {'handlers': handlers, 'module': module} |
172
|
|
|
except Exception as e: |
|
|
|
|
173
|
|
|
err = ('Encountered {} loading plugin at {}: {}\n{}' |
174
|
|
|
.format(type(e).__name__, path, e, format_exc(5))) |
175
|
|
|
error(err) |
176
|
|
|
self._load_errors[path] = err |
177
|
|
|
|
178
|
|
|
def _unload(self): |
179
|
|
|
for path, plugin in self._loaded.items(): |
|
|
|
|
180
|
|
|
handlers = plugin['handlers'] |
181
|
|
|
for handler in handlers: |
182
|
|
|
method_name = handler._nvim_rpc_method_name |
|
|
|
|
183
|
|
|
if hasattr(handler, '_nvim_shutdown_hook'): |
184
|
|
|
handler() |
185
|
|
|
elif handler._nvim_rpc_sync: |
|
|
|
|
186
|
|
|
del self._request_handlers[method_name] |
187
|
|
|
else: |
188
|
|
|
del self._notification_handlers[method_name] |
189
|
|
|
self._specs = {} |
190
|
|
|
self._loaded = {} |
191
|
|
|
|
192
|
|
|
def _discover_classes(self, module, handlers, plugin_path): |
193
|
|
|
for _, cls in inspect.getmembers(module, inspect.isclass): |
194
|
|
|
if getattr(cls, '_nvim_plugin', False): |
195
|
|
|
# create an instance of the plugin and pass the nvim object |
196
|
|
|
plugin = cls(self._configure_nvim_for(cls)) |
197
|
|
|
# discover handlers in the plugin instance |
198
|
|
|
self._discover_functions(plugin, handlers, plugin_path) |
199
|
|
|
|
200
|
|
|
def _discover_functions(self, obj, handlers, plugin_path): |
201
|
|
|
def predicate(o): |
202
|
|
|
return hasattr(o, '_nvim_rpc_method_name') |
203
|
|
|
|
204
|
5 |
|
specs = [] |
205
|
|
|
objdecode = getattr(obj, '_nvim_decode', self._decode_default) |
206
|
|
|
for _, fn in inspect.getmembers(obj, predicate): |
207
|
|
|
sync = fn._nvim_rpc_sync |
|
|
|
|
208
|
|
|
decode = getattr(fn, '_nvim_decode', objdecode) |
209
|
|
|
nvim_bind = None |
210
|
5 |
|
if fn._nvim_bind: |
|
|
|
|
211
|
|
|
nvim_bind = self._configure_nvim_for(fn) |
212
|
|
|
|
213
|
|
|
method = fn._nvim_rpc_method_name |
|
|
|
|
214
|
|
|
if fn._nvim_prefix_plugin_path: |
|
|
|
|
215
|
|
|
method = '{}:{}'.format(plugin_path, method) |
216
|
|
|
|
217
|
5 |
|
fn_wrapped = partial(self._wrap_function, fn, |
218
|
|
|
sync, decode, nvim_bind, method) |
219
|
|
|
self._copy_attributes(fn, fn_wrapped) |
220
|
|
|
# register in the rpc handler dict |
221
|
|
|
if sync: |
222
|
|
|
if method in self._request_handlers: |
223
|
|
|
raise Exception(('Request handler for "{}" is ' + |
224
|
|
|
'already registered').format(method)) |
225
|
|
|
self._request_handlers[method] = fn_wrapped |
226
|
|
|
else: |
227
|
|
|
if method in self._notification_handlers: |
228
|
|
|
raise Exception(('Notification handler for "{}" is ' + |
229
|
|
|
'already registered').format(method)) |
230
|
|
|
self._notification_handlers[method] = fn_wrapped |
231
|
|
|
if hasattr(fn, '_nvim_rpc_spec'): |
232
|
|
|
specs.append(fn._nvim_rpc_spec) |
|
|
|
|
233
|
|
|
handlers.append(fn_wrapped) |
234
|
|
|
if specs: |
235
|
|
|
self._specs[plugin_path] = specs |
236
|
|
|
|
237
|
|
|
def _copy_attributes(self, fn, fn2): |
|
|
|
|
238
|
|
|
# Copy _nvim_* attributes from the original function |
239
|
|
|
for attr in dir(fn): |
240
|
|
|
if attr.startswith('_nvim_'): |
241
|
|
|
setattr(fn2, attr, getattr(fn, attr)) |
242
|
|
|
|
243
|
|
|
def _on_specs_request(self, path): |
244
|
|
|
if IS_PYTHON3: |
245
|
|
|
path = decode_if_bytes(path) |
246
|
|
|
if path in self._load_errors: |
247
|
|
|
self.nvim.out_write(self._load_errors[path] + '\n') |
248
|
|
|
return self._specs.get(path, 0) |
249
|
|
|
|
250
|
|
|
def _configure_nvim_for(self, obj): |
251
|
|
|
# Configure a nvim instance for obj (checks encoding configuration) |
252
|
|
|
nvim = self.nvim |
253
|
|
|
decode = getattr(obj, '_nvim_decode', self._decode_default) |
254
|
|
|
if decode: |
255
|
|
|
nvim = nvim.with_decode(decode) |
256
|
|
|
return nvim |
257
|
|
|
|
Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.
So, unless you specifically plan to handle any error, consider adding a more specific exception.