Completed
Pull Request — master (#189)
by Björn
03:20
created

neovim.plugin.Host._on_request()   A

Complexity

Conditions 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 9.933
Metric Value
cc 3
dl 0
loc 14
ccs 1
cts 12
cp 0.0833
crap 9.933
rs 9.4285
1
"""Implements a Nvim host for python plugins."""
2 6
import functools
3 6
import imp
4 6
import inspect
5 6
import logging
6 6
import os
7 6
import os.path
8 6
import re
9
10 6
from traceback import format_exc
11
12 6
from . import script_host
13 6
from ..api import decode_if_bytes, walk
14 6
from ..compat import IS_PYTHON3, find_module
15 6
from ..msgpack_rpc import ErrorResponse
16
17 6
__all__ = ('Host')
18
19 6
logger = logging.getLogger(__name__)
20 6
error, debug, info, warn = (logger.error, logger.debug, logger.info,
21
                            logger.warning,)
22
23
24 6
class Host(object):
25
26
    """Nvim host for python plugins.
27
28
    Takes care of loading/unloading plugins and routing msgpack-rpc
29
    requests/notifications to the appropriate handlers.
30
    """
31
32 6
    def __init__(self, nvim):
33
        """Set handlers for plugin_load/plugin_unload."""
34
        self.nvim = nvim
35
        self._specs = {}
36
        self._loaded = {}
37
        self._load_errors = {}
38
        self._notification_handlers = {}
39
        self._request_handlers = {
40
            'poll': lambda: 'ok',
41
            'specs': self._on_specs_request,
42
            'shutdown': self.shutdown
43
        }
44
45 6
    def _on_async_err(self, msg):
46
        self.nvim.err_write(msg, async=True)
47
48 6
    def start(self, plugins):
49
        """Start listening for msgpack-rpc requests and notifications."""
50
        self.nvim.run_loop(self._on_request,
51
                           self._on_notification,
52
                           lambda: self._load(plugins),
53
                           err_cb=self._on_async_err)
54
55 6
    def shutdown(self):
56
        """Shutdown the host."""
57
        self._unload()
58
        self.nvim.stop_loop()
59
60 6
    def _on_request(self, name, args):
61
        """Handle a msgpack-rpc request."""
62
        if IS_PYTHON3:
63
            name = decode_if_bytes(name)
64
        handler = self._request_handlers.get(name, None)
65
        if not handler:
66
            msg = self._missing_handler_error(name, 'request')
67
            error(msg)
68
            raise ErrorResponse(msg)
69
70
        debug('calling request handler for "%s", args: "%s"', name, args)
71
        rv = handler(*args)
72
        debug("request handler for '%s %s' returns: %s", name, args, rv)
73
        return rv
74
75 6
    def _on_notification(self, name, args):
76
        """Handle a msgpack-rpc notification."""
77
        if IS_PYTHON3:
78
            name = decode_if_bytes(name)
79
        handler = self._notification_handlers.get(name, None)
80
        if not handler:
81
            msg = self._missing_handler_error(name, 'notification')
82
            error(msg)
83
            self._on_async_err(msg + "\n")
84
            return
85
86
        debug('calling notification handler for "%s", args: "%s"', name, args)
87
        try:
88
            handler(*args)
89
        except Exception as err:
90
            msg = ("error caught in async handler '{} {}':\n{!r}\n{}\n"
91
                   .format(name, args, err, format_exc(5)))
92
            self._on_async_err(msg + "\n")
93
            raise
94
95 6
    def _missing_handler_error(self, name, kind):
96
        msg = 'no {} handler registered for "{}"'.format(kind, name)
97
        pathmatch = re.match(r'(.+):[^:]+:[^:]+', name)
98
        if pathmatch:
99
            loader_error = self._load_errors.get(pathmatch.group(1))
100
            if loader_error is not None:
101
                msg = msg + "\n" + loader_error
102
        return msg
103
104 6
    def _load(self, plugins):
105
        for path in plugins:
106
            err = None
107
            if path in self._loaded:
108
                error('{0} is already loaded'.format(path))
109
                continue
110
            try:
111
                if path == "script_host.py":
112
                    module = script_host
113
                else:
114
                    directory, name = os.path.split(os.path.splitext(path)[0])
115
                    file, pathname, descr = find_module(name, [directory])
116
                    module = imp.load_module(name, file, pathname, descr)
117
                handlers = []
118
                self._discover_classes(module, handlers, path)
119
                self._discover_functions(module, handlers, path)
120
                if not handlers:
121
                    error('{0} exports no handlers'.format(path))
122
                    continue
123
                self._loaded[path] = {'handlers': handlers, 'module': module}
124
            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...
125
                err = ('Encountered {} loading plugin at {}: {}\n{}'
126
                       .format(type(e).__name__, path, e, format_exc(5)))
127
                error(err)
128
                self._load_errors[path] = err
129
130 6
    def _unload(self):
131
        for path, plugin in self._loaded.items():
0 ignored issues
show
Unused Code introduced by
The variable path seems to be unused.
Loading history...
132
            handlers = plugin['handlers']
133
            for handler in handlers:
134
                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...
135
                if hasattr(handler, '_nvim_shutdown_hook'):
136
                    handler()
137
                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...
138
                    del self._request_handlers[method_name]
139
                else:
140
                    del self._notification_handlers[method_name]
141
        self._specs = {}
142
        self._loaded = {}
143
144 6
    def _discover_classes(self, module, handlers, plugin_path):
145
        for _, cls in inspect.getmembers(module, inspect.isclass):
146
            if getattr(cls, '_nvim_plugin', False):
147
                # create an instance of the plugin and pass the nvim object
148
                plugin = cls(self._configure_nvim_for(cls))
149
                # discover handlers in the plugin instance
150
                self._discover_functions(plugin, handlers, plugin_path)
151
152 6
    def _discover_functions(self, obj, handlers, plugin_path):
153
        def predicate(o):
154
            return hasattr(o, '_nvim_rpc_method_name')
155
        specs = []
156
        objdecode = getattr(obj, '_nvim_decode', None)
157
        for _, fn in inspect.getmembers(obj, predicate):
158
            decode = getattr(fn, '_nvim_decode', objdecode)
159
            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...
160
                # bind a nvim instance to the handler
161
                fn2 = functools.partial(fn, self._configure_nvim_for(fn))
162
                # copy _nvim_* attributes from the original function
163
                self._copy_attributes(fn, fn2)
164
                fn = fn2
165
            decode = self._should_decode(decode)
166
            if decode:
167
                decoder = lambda fn, decode, *args: fn(*walk(decode_if_bytes, args, decode))
168
                fn2 = functools.partial(decoder, fn, decode)
169
                self._copy_attributes(fn, fn2)
170
                fn = fn2
171
172
            # register in the rpc handler dict
173
            method = fn._nvim_rpc_method_name
0 ignored issues
show
Bug introduced by
The Function newfunc does not seem to have a member named _nvim_rpc_method_name.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
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...
174
            if fn._nvim_prefix_plugin_path:
0 ignored issues
show
Bug introduced by
The Function newfunc does not seem to have a member named _nvim_prefix_plugin_path.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
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...
175
                method = '{0}:{1}'.format(plugin_path, method)
176
            if fn._nvim_rpc_sync:
0 ignored issues
show
Bug introduced by
The Function newfunc does not seem to have a member named _nvim_rpc_sync.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
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...
177
                if method in self._request_handlers:
178
                    raise Exception(('Request handler for "{0}" is ' +
179
                                    'already registered').format(method))
180
                self._request_handlers[method] = fn
181
            else:
182
                if method in self._notification_handlers:
183
                    raise Exception(('Notification handler for "{0}" is ' +
184
                                    'already registered').format(method))
185
                self._notification_handlers[method] = fn
186
            if hasattr(fn, '_nvim_rpc_spec'):
187
                specs.append(fn._nvim_rpc_spec)
0 ignored issues
show
Bug introduced by
The Function newfunc does not seem to have a member named _nvim_rpc_spec.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
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...
188
            handlers.append(fn)
189
        if specs:
190
            self._specs[plugin_path] = specs
191
192 6
    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...
193
        # Copy _nvim_* attributes from the original function
194
        for attr in dir(fn):
195
            if attr.startswith('_nvim_'):
196
                setattr(fn2, attr, getattr(fn, attr))
197
198 6
    def _on_specs_request(self, path):
199
        if IS_PYTHON3:
200
            name = decode_if_bytes(name)
0 ignored issues
show
Bug introduced by
The variable name was used before it was assigned.
Loading history...
201
        if path in self._load_errors:
202
            self.nvim.out_write(self._load_errors[path] + '\n')
203
        return self._specs.get(path, 0)
204
205 6
    def _should_decode(self, decode):
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...
206
        if decode is None:
207
            decode = IS_PYTHON3
208
        return decode
209
210 6
    def _configure_nvim_for(self, obj):
211
        # Configure a nvim instance for obj (checks encoding configuration)
212
        nvim = self.nvim
213
        decode = getattr(obj, '_nvim_decode', None)
214
        decode = self._should_decode(decode)
215
        if decode:
216
            nvim = nvim.with_decode(decode)
217
        return nvim
218