Completed
Pull Request — master (#262)
by Björn
23:20
created

Host._check_queued()   A

Complexity

Conditions 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
c 1
b 0
f 0
dl 0
loc 14
ccs 0
cts 12
cp 0
crap 20
rs 9.2
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:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

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.

Loading history...
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:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

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.

Loading history...
Unused Code introduced by
The variable e seems to be unused.
Loading history...
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:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

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.

Loading history...
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():
0 ignored issues
show
Unused Code introduced by
The variable path seems to be unused.
Loading history...
180
            handlers = plugin['handlers']
181
            for handler in handlers:
182
                method_name = handler._nvim_rpc_method_name
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_rpc_method_name was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
183
                if hasattr(handler, '_nvim_shutdown_hook'):
184
                    handler()
185
                elif handler._nvim_rpc_sync:
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_rpc_sync was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
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
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_rpc_sync was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
208
            decode = getattr(fn, '_nvim_decode', objdecode)
209
            nvim_bind = None
210 5
            if fn._nvim_bind:
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_bind was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
211
                nvim_bind = self._configure_nvim_for(fn)
212
213
            method = fn._nvim_rpc_method_name
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_rpc_method_name was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
214
            if fn._nvim_prefix_plugin_path:
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_prefix_plugin_path was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
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)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_rpc_spec was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
233
            handlers.append(fn_wrapped)
234
        if specs:
235
            self._specs[plugin_path] = specs
236
237
    def _copy_attributes(self, fn, fn2):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
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