Completed
Pull Request — master (#179)
by Björn
02:42
created

neovim.plugin.Host._on_async_err()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

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