Completed
Push — master ( 2d9fb2...c7025e )
by Björn
8s
created

neovim.plugin.Host._missing_handler_error()   A

Complexity

Conditions 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 9.0292
Metric Value
cc 3
dl 0
loc 8
ccs 1
cts 8
cp 0.125
crap 9.0292
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 ..api import DecodeHook
13 6
from ..compat import IS_PYTHON3, find_module
14 6
from ..msgpack_rpc import ErrorResponse
15
16 6
__all__ = ('Host')
17
18 6
logger = logging.getLogger(__name__)
19 6
error, debug, info, warn = (logger.error, logger.debug, logger.info,
20
                            logger.warning,)
21
22
23 6
class Host(object):
24
25
    """Nvim host for python plugins.
26
27
    Takes care of loading/unloading plugins and routing msgpack-rpc
28
    requests/notifications to the appropriate handlers.
29
    """
30
31 6
    def __init__(self, nvim):
32
        """Set handlers for plugin_load/plugin_unload."""
33
        self.nvim = nvim
34
        self._specs = {}
35
        self._loaded = {}
36
        self._load_errors = {}
37
        self._notification_handlers = {}
38
        self._request_handlers = {
39
            'poll': lambda: 'ok',
40
            'specs': self._on_specs_request,
41
            'shutdown': self.shutdown
42
        }
43
        self._nvim_encoding = nvim.options['encoding']
44
        if IS_PYTHON3 and isinstance(self._nvim_encoding, bytes):
45
            self._nvim_encoding = self._nvim_encoding.decode('ascii')
46
47 6
    def start(self, plugins):
48
        """Start listening for msgpack-rpc requests and notifications."""
49
        self.nvim.session.run(self._on_request,
50
                              self._on_notification,
51
                              lambda: self._load(plugins))
52
53 6
    def shutdown(self):
54
        """Shutdown the host."""
55
        self._unload()
56
        self.nvim.session.stop()
57
58 6
    def _on_request(self, name, args):
59
        """Handle a msgpack-rpc request."""
60
        if IS_PYTHON3 and isinstance(name, bytes):
61
            name = name.decode(self._nvim_encoding)
62
        handler = self._request_handlers.get(name, None)
63
        if not handler:
64
            msg = self._missing_handler_error(name, 'request')
65
            error(msg)
66
            raise ErrorResponse(msg)
67
68
        debug('calling request handler for "%s", args: "%s"', name, args)
69
        rv = handler(*args)
70
        debug("request handler for '%s %s' returns: %s", name, args, rv)
71
        return rv
72
73 6
    def _on_notification(self, name, args):
74
        """Handle a msgpack-rpc notification."""
75
        if IS_PYTHON3 and isinstance(name, bytes):
76
            name = name.decode(self._nvim_encoding)
77
        handler = self._notification_handlers.get(name, None)
78
        if not handler:
79
            msg = self._missing_handler_error(name, 'notification')
80
            error(msg)
81
            self.nvim.err_write(msg + "\n")
82
            return
83
84
        debug('calling notification handler for "%s", args: "%s"', name, args)
85
        try:
86
            handler(*args)
87
        except Exception as err:
88
            msg = ("error caught in async handler '{} {}':\n{!r}\n{}"
89
                   .format(name, args, err, format_exc(5)))
90
            self.nvim.err_write(msg, async=True)
91
            raise
92
93 6
    def _missing_handler_error(self, name, kind):
94
        msg = 'no {} handler registered for "{}"'.format(kind, name)
95
        pathmatch = re.match(r'(.+):[^:]+:[^:]+', name)
96
        if pathmatch:
97
            loader_error = self._load_errors.get(pathmatch.group(1))
98
            if loader_error is not None:
99
                msg = msg + "\n" + loader_error
100
        return msg
101
102 6
    def _load(self, plugins):
103
        for path in plugins:
104
            err = None
105
            if path in self._loaded:
106
                error('{0} is already loaded'.format(path))
107
                continue
108
            if path == "script_host.py":
109
                directory = os.path.dirname(__file__)
110
                name = "script_host"
111
            else:
112
                directory, name = os.path.split(os.path.splitext(path)[0])
113
            file, pathname, description = find_module(name, [directory])
114
            handlers = []
115
            try:
116
                module = imp.load_module(name, file, pathname, description)
117
                self._discover_classes(module, handlers, path)
118
                self._discover_functions(module, handlers, path)
119
                if not handlers:
120
                    error('{0} exports no handlers'.format(path))
121
                    continue
122
                self._loaded[path] = {'handlers': handlers, 'module': module}
123
            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...
124
                err = ('Encountered {} loading plugin at {}: {}\n{}'
125
                       .format(type(e).__name__, path, e, format_exc(5)))
126
                error(err)
127
                self._load_errors[path] = err
128
129 6
    def _unload(self):
130
        for path, plugin in self._loaded.items():
0 ignored issues
show
Unused Code introduced by
The variable path seems to be unused.
Loading history...
131
            handlers = plugin['handlers']
132
            for handler in handlers:
133
                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...
134
                if hasattr(handler, '_nvim_shutdown_hook'):
135
                    handler()
136
                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...
137
                    del self._request_handlers[method_name]
138
                else:
139
                    del self._notification_handlers[method_name]
140
        self._specs = {}
141
        self._loaded = {}
142
143 6
    def _discover_classes(self, module, handlers, plugin_path):
144
        for _, cls in inspect.getmembers(module, inspect.isclass):
145
            if getattr(cls, '_nvim_plugin', False):
146
                # create an instance of the plugin and pass the nvim object
147
                plugin = cls(self._configure_nvim_for(cls))
148
                # discover handlers in the plugin instance
149
                self._discover_functions(plugin, handlers, plugin_path)
150
151 6
    def _discover_functions(self, obj, handlers, plugin_path):
152
        def predicate(o):
153
            return hasattr(o, '_nvim_rpc_method_name')
154
        specs = []
155
        objenc = getattr(obj, '_nvim_encoding', None)
156
        for _, fn in inspect.getmembers(obj, predicate):
157
            enc = getattr(fn, '_nvim_encoding', objenc)
158
            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...
159
                # bind a nvim instance to the handler
160
                fn2 = functools.partial(fn, self._configure_nvim_for(fn))
161
                # copy _nvim_* attributes from the original function
162
                self._copy_attributes(fn, fn2)
163
                fn = fn2
164
            decodehook = self._decodehook_for(enc)
165
            if decodehook is not None:
166
                decoder = lambda fn, hook, *args: fn(*hook.walk(args))
167
                fn2 = functools.partial(decoder, fn, decodehook)
168
                self._copy_attributes(fn, fn2)
169
                fn = fn2
170
171
            # register in the rpc handler dict
172
            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...
173
            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...
174
                method = '{0}:{1}'.format(plugin_path, method)
175
            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...
176
                if method in self._request_handlers:
177
                    raise Exception(('Request handler for "{0}" is ' +
178
                                    'already registered').format(method))
179
                self._request_handlers[method] = fn
180
            else:
181
                if method in self._notification_handlers:
182
                    raise Exception(('Notification handler for "{0}" is ' +
183
                                    'already registered').format(method))
184
                self._notification_handlers[method] = fn
185
            if hasattr(fn, '_nvim_rpc_spec'):
186
                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...
187
            handlers.append(fn)
188
        if specs:
189
            self._specs[plugin_path] = specs
190
191 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...
192
        # Copy _nvim_* attributes from the original function
193
        for attr in dir(fn):
194
            if attr.startswith('_nvim_'):
195
                setattr(fn2, attr, getattr(fn, attr))
196
197 6
    def _on_specs_request(self, path):
198
        if IS_PYTHON3 and isinstance(path, bytes):
199
            path = path.decode(self._nvim_encoding)
200
        if path in self._load_errors:
201
            self.nvim.out_write(self._load_errors[path] + '\n')
202
        return self._specs.get(path, 0)
203
204 6
    def _decodehook_for(self, encoding):
205
        if IS_PYTHON3 and encoding is None:
206
            encoding = True
207
        if encoding is True:
208
            encoding = self._nvim_encoding
209
        if encoding:
210
            return DecodeHook(encoding)
211
212 6
    def _configure_nvim_for(self, obj):
213
        # Configure a nvim instance for obj (checks encoding configuration)
214
        nvim = self.nvim
215
        encoding = getattr(obj, '_nvim_encoding', None)
216
        hook = self._decodehook_for(encoding)
217
        if hook is not None:
218
            nvim = nvim.with_hook(hook)
219
        return nvim
220